001package org.hl7.fhir.dstu3.utils;
002
003import java.io.UnsupportedEncodingException;
004import java.net.URLDecoder;
005import java.util.ArrayList;
006import java.util.HashMap;
007import java.util.List;
008import java.util.Map;
009
010import org.hl7.fhir.dstu3.context.IWorkerContext;
011
012/*
013  Copyright (c) 2011+, HL7, Inc.
014  All rights reserved.
015  
016  Redistribution and use in source and binary forms, with or without modification, 
017  are permitted provided that the following conditions are met:
018    
019   * Redistributions of source code must retain the above copyright notice, this 
020     list of conditions and the following disclaimer.
021   * Redistributions in binary form must reproduce the above copyright notice, 
022     this list of conditions and the following disclaimer in the documentation 
023     and/or other materials provided with the distribution.
024   * Neither the name of HL7 nor the names of its contributors may be used to 
025     endorse or promote products derived from this software without specific 
026     prior written permission.
027  
028  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
029  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
030  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
031  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
032  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
033  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
034  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
035  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
036  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
037  POSSIBILITY OF SUCH DAMAGE.
038  
039 */
040import org.hl7.fhir.dstu3.model.BackboneElement;
041import org.hl7.fhir.dstu3.model.Base;
042import org.hl7.fhir.dstu3.model.Bundle;
043import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
044import org.hl7.fhir.dstu3.model.Bundle.BundleLinkComponent;
045import org.hl7.fhir.dstu3.model.DomainResource;
046import org.hl7.fhir.dstu3.model.Element;
047import org.hl7.fhir.dstu3.model.ExpressionNode;
048import org.hl7.fhir.dstu3.model.IntegerType;
049import org.hl7.fhir.dstu3.model.Property;
050import org.hl7.fhir.dstu3.model.Reference;
051import org.hl7.fhir.dstu3.model.Resource;
052import org.hl7.fhir.dstu3.model.StringType;
053import org.hl7.fhir.exceptions.FHIRException;
054import org.hl7.fhir.instance.model.api.IBaseResource;
055import org.hl7.fhir.utilities.Utilities;
056import org.hl7.fhir.utilities.graphql.Argument;
057import org.hl7.fhir.utilities.graphql.Argument.ArgumentListStatus;
058import org.hl7.fhir.utilities.graphql.Directive;
059import org.hl7.fhir.utilities.graphql.EGraphEngine;
060import org.hl7.fhir.utilities.graphql.EGraphQLException;
061import org.hl7.fhir.utilities.graphql.Field;
062import org.hl7.fhir.utilities.graphql.Fragment;
063import org.hl7.fhir.utilities.graphql.GraphQLResponse;
064import org.hl7.fhir.utilities.graphql.IGraphQLEngine;
065import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
066import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices.ReferenceResolution;
067import org.hl7.fhir.utilities.graphql.NameValue;
068import org.hl7.fhir.utilities.graphql.NumberValue;
069import org.hl7.fhir.utilities.graphql.ObjectValue;
070import org.hl7.fhir.utilities.graphql.Operation;
071import org.hl7.fhir.utilities.graphql.Operation.OperationType;
072import org.hl7.fhir.utilities.graphql.Package;
073import org.hl7.fhir.utilities.graphql.Selection;
074import org.hl7.fhir.utilities.graphql.StringValue;
075import org.hl7.fhir.utilities.graphql.Value;
076import org.hl7.fhir.utilities.graphql.Variable;
077import org.hl7.fhir.utilities.graphql.VariableValue;
078
079public class GraphQLEngine implements IGraphQLEngine {
080  
081  public static class SearchEdge extends Base {
082
083    private BundleEntryComponent be;
084    private String type;
085    
086    SearchEdge(String type, BundleEntryComponent be) {
087      this.type = type;
088      this.be = be;
089    }
090    @Override
091    public String fhirType() {
092      return type;
093    }
094
095    @Override
096    protected void listChildren(List<Property> result) {
097      throw new Error("Not Implemented");
098    }
099
100    @Override
101    public String getIdBase() {
102      throw new Error("Not Implemented");
103    }
104
105    @Override
106    public void setIdBase(String value) {
107      throw new Error("Not Implemented");
108    }
109
110    @Override
111    public Property getNamedProperty(int _hash, String _name, boolean _checkValid) throws FHIRException {
112      switch (_hash) {
113      case 3357091:    /*mode*/     return new Property(_name, "string",   "n/a", 0, 1, be.getSearch().hasMode() ? be.getSearch().getModeElement() : null);
114      case 109264530:  /*score*/    return new Property(_name, "string",   "n/a", 0, 1, be.getSearch().hasScore() ? be.getSearch().getScoreElement() : null);
115      case -341064690: /*resource*/ return new Property(_name, "resource",  "n/a", 0, 1, be.hasResource() ? be.getResource() : null);
116      default: return super.getNamedProperty(_hash, _name, _checkValid);
117      }
118    }
119  }
120
121  public static class SearchWrapper extends Base {
122
123    private Bundle bnd;
124    private String type;
125    private Map<String, String> map;
126
127    SearchWrapper(String type, Bundle bnd) throws FHIRException {
128      this.type = type;
129      this.bnd = bnd;
130      for (BundleLinkComponent bl : bnd.getLink()) 
131        if (bl.getRelation().equals("self"))
132          map = parseURL(bl.getUrl());
133    }
134
135    @Override
136    public String fhirType() {
137      return type;
138    }
139
140    @Override
141    protected void listChildren(List<Property> result) {
142      throw new Error("Not Implemented");
143    }
144
145    @Override
146    public String getIdBase() {
147      throw new Error("Not Implemented");
148    }
149
150    @Override
151    public void setIdBase(String value) {
152      throw new Error("Not Implemented");
153    }
154
155    @Override
156    public Property getNamedProperty(int _hash, String _name, boolean _checkValid) throws FHIRException {
157      switch (_hash) {
158      case 97440432:   /*first*/     return new Property(_name, "string",  "n/a", 0, 1, extractLink(_name));
159      case -1273775369: /*previous*/  return new Property(_name, "string",  "n/a", 0, 1, extractLink(_name));
160      case 3377907:    /*next*/      return new Property(_name, "string",  "n/a", 0, 1, extractLink(_name));
161      case 3314326:    /*last*/      return new Property(_name, "string",  "n/a", 0, 1, extractLink(_name));
162      case 94851343:   /*count*/     return new Property(_name, "integer", "n/a", 0, 1, bnd.getTotalElement());
163      case -1019779949:/*offset*/    return new Property(_name, "integer", "n/a", 0, 1, extractParam("search-offset"));
164      case 860381968:  /*pagesize*/  return new Property(_name, "integer", "n/a", 0, 1, extractParam("_count"));
165      case 96356950:  /*edges*/      return new Property(_name, "edge",    "n/a", 0, Integer.MAX_VALUE, getEdges());
166      default: return super.getNamedProperty(_hash, _name, _checkValid);
167      }
168    }
169
170    private List<Base> getEdges() {
171      List<Base> list = new ArrayList<>();
172      for (BundleEntryComponent be : bnd.getEntry())
173        list.add(new SearchEdge(type.substring(0, type.length() - 10) + "Edge", be));
174      return list;
175    }
176
177    private Base extractParam(String name) throws FHIRException {
178      return map != null ? new IntegerType(map.get(name)) : null;
179    }
180
181    private Map<String, String> parseURL(String url) throws FHIRException {
182      try {
183        Map<String, String> map = new HashMap<String, String>();
184        String[] pairs = url.split("&");
185        for (String pair : pairs) {
186          int idx = pair.indexOf("=");
187          String key;
188          key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair;
189          String value = idx > 0 && pair.length() > idx + 1 ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null;
190          map.put(key, value);
191        }
192        return map;
193      } catch (UnsupportedEncodingException e) {
194        throw new FHIRException(e);
195      }
196    }
197
198    private Base extractLink(String _name) throws FHIRException {
199      for (BundleLinkComponent bl : bnd.getLink()) {
200        if (bl.getRelation().equals(_name)) {
201          Map<String, String> map = parseURL(bl.getUrl());
202          return new StringType(map.get("search-id")+':'+map.get("search-offset"));
203        }
204      }
205      return null;
206    }
207
208  }
209  
210  private IWorkerContext context;
211  
212  public GraphQLEngine(IWorkerContext context) {
213    super();
214    this.context = context;
215  }
216
217  /**
218   *  for the host to pass context into and get back on the reference resolution interface
219   */
220  private Object appInfo;
221
222  /**
223   *  the focus resource - if (there instanceof one. if (there isn"t,) there instanceof no focus
224   */
225  private Resource focus;
226
227  /**
228   * The package that describes the graphQL to be executed, operation name, and variables
229   */
230  private Package graphQL;
231
232  /**
233   * where the output from executing the query instanceof going to go
234   */
235  private GraphQLResponse output;
236
237  /** 
238   * Application provided reference resolution services 
239   */
240  private IGraphQLStorageServices services;
241
242  // internal stuff 
243  private Map<String, Argument> workingVariables = new HashMap<String, Argument>();
244
245  private FHIRPathEngine fpe;
246
247  private ExpressionNode magicExpression;
248  
249  public void execute() throws EGraphEngine, EGraphQLException, FHIRException {
250    if (graphQL == null)
251      throw new EGraphEngine("Unable to process graphql - graphql document missing");
252    fpe = new FHIRPathEngine(this.context);
253    magicExpression = new ExpressionNode(0);
254
255    output = new GraphQLResponse();
256
257    Operation op = null;
258    // todo: initial conditions
259    if (!Utilities.noString(graphQL.getOperationName())) {
260      op = graphQL.getDocument().operation(graphQL.getOperationName());
261      if (op == null)
262        throw new EGraphEngine("Unable to find operation \""+graphQL.getOperationName()+"\"");
263    } else if ((graphQL.getDocument().getOperations().size() == 1))
264      op = graphQL.getDocument().getOperations().get(0);
265    else
266      throw new EGraphQLException("No operation name provided, so expected to find a single operation");
267
268    if (op.getOperationType() == OperationType.qglotMutation)
269      throw new EGraphQLException("Mutation operations are not supported (yet)");
270
271    checkNoDirectives(op.getDirectives());
272    processVariables(op);
273    if (focus == null)
274      processSearch(output, op.getSelectionSet(), false, "");
275    else
276      processObject(focus, focus, output, op.getSelectionSet(), false, "");
277  }
278
279  private boolean checkBooleanDirective(Directive dir) throws EGraphQLException {
280    if (dir.getArguments().size() != 1)
281      throw new EGraphQLException("Unable to process @"+dir.getName()+": expected a single argument \"if\"");
282    if (!dir.getArguments().get(0).getName().equals("if"))
283      throw new EGraphQLException("Unable to process @"+dir.getName()+": expected a single argument \"if\"");
284    List<Value> vl = resolveValues(dir.getArguments().get(0), 1);
285    return vl.get(0).toString().equals("true");
286  }
287
288  private boolean checkDirectives(List<Directive> directives) throws EGraphQLException {
289    Directive skip = null;
290    Directive include = null;
291    for (Directive dir : directives) {
292      if (dir.getName().equals("skip")) {
293        if ((skip == null))
294          skip = dir;
295        else
296          throw new EGraphQLException("Duplicate @skip directives");
297      } else if (dir.getName().equals("include")) {
298        if ((include == null))
299          include = dir;
300        else
301          throw new EGraphQLException("Duplicate @include directives");
302      }
303      else if (!Utilities.existsInList(dir.getName(), "flatten", "first", "singleton", "slice"))
304        throw new EGraphQLException("Directive \""+dir.getName()+"\" instanceof not recognised");
305    }
306    if ((skip != null && include != null))
307      throw new EGraphQLException("Cannot mix @skip and @include directives");
308    if (skip != null)
309      return !checkBooleanDirective(skip);
310    else if (include != null)
311      return checkBooleanDirective(include);
312    else
313      return true;
314  }
315
316  private void checkNoDirectives(List<Directive> directives) {
317
318  }
319
320  private boolean targetTypeOk(List<Argument> arguments, IBaseResource dest) throws EGraphQLException {
321    List<String> list = new ArrayList<String>();
322    for (Argument arg : arguments) {
323      if ((arg.getName().equals("type"))) {
324        List<Value> vl = resolveValues(arg);
325        for (Value v : vl)
326          list.add(v.toString());
327      }
328    }
329    if (list.size() == 0)
330      return true;
331    else
332      return list.indexOf(dest.fhirType()) > -1;
333  }
334
335  private boolean hasExtensions(Base obj) {
336    if (obj instanceof BackboneElement)
337      return ((BackboneElement) obj).getExtension().size() > 0 || ((BackboneElement) obj).getModifierExtension().size() > 0;
338      else if (obj instanceof DomainResource)
339        return ((DomainResource)obj).getExtension().size() > 0 || ((DomainResource)obj).getModifierExtension().size() > 0;
340        else if (obj instanceof Element)
341          return ((Element)obj).getExtension().size() > 0;
342          else
343            return false;
344  }
345
346  private boolean passesExtensionMode(Base obj, boolean extensionMode) {
347    if (!obj.isPrimitive())
348      return !extensionMode;
349    else if (extensionMode)
350      return !Utilities.noString(obj.getIdBase()) || hasExtensions(obj);
351    else
352      return obj.primitiveValue() != "";
353  }
354
355  private List<Base> filter(Resource context, Property prop, String fieldName, List<Argument> arguments, List<Base> values, boolean extensionMode) throws FHIRException, EGraphQLException {
356    List<Base> result = new ArrayList<Base>();
357    if (values.size() > 0) {
358      int count = Integer.MAX_VALUE;
359      int offset = 0;
360      StringBuilder fp = new StringBuilder();
361      for (Argument arg : arguments) {
362        List<Value> vl = resolveValues(arg);
363        if ((vl.size() != 1))
364          throw new EGraphQLException("Incorrect number of arguments");
365        if (values.get(0).isPrimitive())
366          throw new EGraphQLException("Attempt to use a filter ("+arg.getName()+") on a primtive type ("+prop.getTypeCode()+")");
367        if ((arg.getName().equals("fhirpath")))
368          fp.append(" and "+vl.get(0).toString());
369        else if ((arg.getName().equals("_count")))
370          count = Integer.valueOf(vl.get(0).toString());
371        else if ((arg.getName().equals("_offset")))
372          offset = Integer.valueOf(vl.get(0).toString());
373        else {
374          Property p = values.get(0).getNamedProperty(arg.getName());
375          if (p == null)
376            throw new EGraphQLException("Attempt to use an unknown filter ("+arg.getName()+") on a type ("+prop.getTypeCode()+")");
377          fp.append(" and "+arg.getName()+" = '"+vl.get(0).toString()+"'");
378        }
379      }
380
381      // Account for situations where the GraphQL expression selected e.g.
382      // effectiveDateTime but the field contains effectivePeriod
383      String propName = prop.getName();
384      List<Base> newValues = new ArrayList<>(values.size());
385      for (Base value : values) {
386        if (propName.endsWith("[x]")) {
387          String propNameShortened = propName.substring(0, propName.length() - 3);
388          if (fieldName.startsWith(propNameShortened) && fieldName.length() > propNameShortened.length()) {
389            if (!value.fhirType().equalsIgnoreCase(fieldName.substring(propNameShortened.length()))) {
390              continue;
391            }
392          }
393        }
394        newValues.add(value);
395      }
396
397      int i = 0;
398      int t = 0;
399      if (fp.length() == 0)
400        for (Base v : newValues) {
401          if ((i >= offset) && passesExtensionMode(v, extensionMode)) {
402            result.add(v);
403            t++;
404            if (t >= count)
405              break;
406          }
407          i++;
408        } else {
409          ExpressionNode node = fpe.parse(fp.substring(5));
410          for (Base v : newValues) {
411            if ((i >= offset) && passesExtensionMode(v, extensionMode) && fpe.evaluateToBoolean(null, context, v, node)) {
412              result.add(v);
413              t++;
414              if (t >= count)
415                break;
416            }
417            i++;
418          }
419        }
420    }
421    return result;
422  }
423
424  private List<Resource> filterResources(Argument fhirpath, Bundle bnd) throws EGraphQLException, FHIRException {
425    List<Resource> result = new ArrayList<Resource>();
426    if (bnd.getEntry().size() > 0) {
427      if ((fhirpath == null))
428        for (BundleEntryComponent be : bnd.getEntry())
429          result.add(be.getResource());
430      else {
431        FHIRPathEngine fpe = new FHIRPathEngine(context);
432        ExpressionNode node = fpe.parse(getSingleValue(fhirpath));
433        for (BundleEntryComponent be : bnd.getEntry())
434          if (fpe.evaluateToBoolean(null, be.getResource(), be.getResource(), node))
435            result.add(be.getResource());
436      }
437    }
438    return result;
439  }
440
441  private List<Resource> filterResources(Argument fhirpath, List<IBaseResource> list) throws EGraphQLException, FHIRException {
442    List<Resource> result = new ArrayList<>();
443    if (list.size() > 0) {
444      if ((fhirpath == null))
445        for (IBaseResource v : list)
446          result.add((Resource) v);
447      else {
448        FHIRPathEngine fpe = new FHIRPathEngine(context);
449        ExpressionNode node = fpe.parse(getSingleValue(fhirpath));
450        for (IBaseResource v : list)
451          if (fpe.evaluateToBoolean(null, (Resource)v, (Resource)v, node))
452            result.add((Resource) v);
453      }
454    }
455    return result;
456  }
457
458  private boolean hasArgument(List<Argument> arguments, String name, String value) {
459    for (Argument arg : arguments)
460      if ((arg.getName().equals(name)) && arg.hasValue(value))
461        return true;
462    return false;
463  }
464
465  private void processValues(Resource context, Selection sel, Property prop, ObjectValue target, List<Base> values, boolean extensionMode, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
466    boolean il = false;
467    Argument arg = null;
468    ExpressionNode expression = null;
469    if (sel.getField().hasDirective("slice")) {
470      Directive dir = sel.getField().directive("slice");
471      String s = ((StringValue) dir.getArguments().get(0).getValues().get(0)).getValue();
472      if (s.equals("$index"))
473        expression = magicExpression;
474      else
475        expression = fpe.parse(s);
476    }
477    if (sel.getField().hasDirective("flatten")) // special: instruction to drop this node...
478      il = prop.isList() && !sel.getField().hasDirective("first");
479    else if (sel.getField().hasDirective("first")) {
480      if (expression != null) 
481        throw new FHIRException("You cannot mix @slice and @first");
482      arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), inheritedList));
483    } else if (expression == null)
484      arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), prop.isList() || inheritedList));
485
486    
487    int index = 0;
488    for (Base value : values) {
489      String ss = "";
490      if (expression != null) {
491        if (expression == magicExpression)
492          ss = suffix+'.'+Integer.toString(index);
493        else
494          ss = suffix+'.'+fpe.evaluateToString(null, null, value, expression);
495        if (!sel.getField().hasDirective("flatten"))
496          arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), prop.isList() || inheritedList));
497      }
498
499      if (value.isPrimitive() && !extensionMode) {
500        if (!sel.getField().getSelectionSet().isEmpty())
501          throw new EGraphQLException("Encountered a selection set on a scalar field type");
502        processPrimitive(arg, value);
503      } else {
504        if (sel.getField().getSelectionSet().isEmpty())
505          throw new EGraphQLException("No Fields selected on a complex object");
506        if (arg == null)
507          processObject(context, value, target, sel.getField().getSelectionSet(), il, ss);
508        else {
509          ObjectValue n = new ObjectValue();
510          arg.addValue(n);
511          processObject(context, value, n, sel.getField().getSelectionSet(), il, ss);
512        }
513      }
514      if (sel.getField().hasDirective("first"))
515        return;
516      index++;
517    }
518  }
519
520  private void processVariables(Operation op) throws EGraphQLException {
521    for (Variable varRef : op.getVariables()) {
522      Argument varDef = null;
523      for (Argument v : graphQL.getVariables()) 
524        if (v.getName().equals(varRef.getName()))
525          varDef = v;
526      if (varDef != null)
527        workingVariables.put(varRef.getName(), varDef); // todo: check type?
528      else if (varRef.getDefaultValue() != null)
529        workingVariables.put(varRef.getName(), new Argument(varRef.getName(), varRef.getDefaultValue()));
530      else
531        throw new EGraphQLException("No value found for variable ");
532    }
533  }
534
535  private boolean isPrimitive(String typename) {
536    return Utilities.existsInList(typename, "boolean", "integer", "string", "decimal", "uri", "base64Binary", "instant", "date", "dateTime", "time", "code", "oid", "id", "markdown", "unsignedInt", "positiveInt", "url", "canonical");
537  }
538
539  private boolean isResourceName(String name, String suffix) {
540    if (!name.endsWith(suffix))
541      return false;
542    name = name.substring(0, name.length()-suffix.length());
543    return context.getResourceNamesAsSet().contains(name);
544  }
545
546  private void processObject(Resource context, Base source, ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
547    for (Selection sel : selection) {
548      if (sel.getField() != null) {
549        if (checkDirectives(sel.getField().getDirectives())) {
550          Property prop = source.getNamedProperty(sel.getField().getName());
551          if ((prop == null) && sel.getField().getName().startsWith("_"))
552            prop = source.getNamedProperty(sel.getField().getName().substring(1));
553          if (prop == null) {
554            if ((sel.getField().getName().equals("resourceType") && source instanceof Resource))
555              target.addField("resourceType", listStatus(sel.getField(), false)).addValue(new StringValue(source.fhirType()));
556            else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("Reference")))
557              processReference(context, source, sel.getField(), target, inheritedList, suffix);
558            else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("canonical")))
559              processCanonicalReference(context, source, sel.getField(), target, inheritedList, suffix);
560            else if (isResourceName(sel.getField().getName(), "List") && (source instanceof Resource))
561              processReverseReferenceList((Resource) source, sel.getField(), target, inheritedList, suffix);
562            else if (isResourceName(sel.getField().getName(), "Connection") && (source instanceof Resource))
563              processReverseReferenceSearch((Resource) source, sel.getField(), target, inheritedList, suffix);
564            else
565              throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType());
566          } else {
567            if (!isPrimitive(prop.getTypeCode()) && sel.getField().getName().startsWith("_"))
568              throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType());
569
570            List<Base> vl = filter(context, prop, sel.getField().getName(), sel.getField().getArguments(), prop.getValues(), sel.getField().getName().startsWith("_"));
571            if (!vl.isEmpty())
572              processValues(context, sel, prop, target, vl, sel.getField().getName().startsWith("_"), inheritedList, suffix);
573          }
574        }
575      } else if (sel.getInlineFragment() != null) {
576        if (checkDirectives(sel.getInlineFragment().getDirectives())) {
577          if (Utilities.noString(sel.getInlineFragment().getTypeCondition()))
578            throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid?
579          if (source.fhirType().equals(sel.getInlineFragment().getTypeCondition())) 
580            processObject(context, source, target, sel.getInlineFragment().getSelectionSet(), inheritedList, suffix);
581        }
582      } else if (checkDirectives(sel.getFragmentSpread().getDirectives())) {
583        Fragment fragment = graphQL.getDocument().fragment(sel.getFragmentSpread().getName());
584        if (fragment == null)
585          throw new EGraphQLException("Unable to resolve fragment "+sel.getFragmentSpread().getName());
586
587        if (Utilities.noString(fragment.getTypeCondition()))
588          throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid?
589        if (source.fhirType().equals(fragment.getTypeCondition()))
590          processObject(context, source, target, fragment.getSelectionSet(), inheritedList, suffix);
591      }
592    }
593  }
594
595  private void processPrimitive(Argument arg, Base value) {
596    String s = value.fhirType();
597    if (s.equals("integer") || s.equals("decimal") || s.equals("unsignedInt") || s.equals("positiveInt"))
598      arg.addValue(new NumberValue(value.primitiveValue()));
599    else if (s.equals("boolean"))
600      arg.addValue(new NameValue(value.primitiveValue()));
601    else
602      arg.addValue(new StringValue(value.primitiveValue()));
603  }
604
605  private void processReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
606    if (!(source instanceof Reference))
607      throw new EGraphQLException("Not done yet");
608    if (services == null)
609      throw new EGraphQLException("Resource Referencing services not provided");
610
611    Reference ref = (Reference) source;
612    ReferenceResolution res = services.lookup(appInfo, context, ref);
613    if (res != null) {
614      if (targetTypeOk(field.getArguments(), res.getTarget())) {
615        Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList));
616        ObjectValue obj = new ObjectValue();
617        arg.addValue(obj);
618        processObject((Resource)res.getTargetContext(), (Base) res.getTarget(), obj, field.getSelectionSet(), inheritedList, suffix);
619      }
620    }
621    else if (!hasArgument(field.getArguments(), "optional", "true"))
622      throw new EGraphQLException("Unable to resolve reference to "+ref.getReference());
623  }
624
625  private void processCanonicalReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
626    if (services == null)
627      throw new EGraphQLException("Resource Referencing services not provided");
628
629    Reference ref = new Reference(source.primitiveValue());
630    ReferenceResolution res = services.lookup(appInfo, context, ref);
631    if (res != null) {
632      if (targetTypeOk(field.getArguments(), res.getTarget())) {
633        Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList));
634        ObjectValue obj = new ObjectValue();
635        arg.addValue(obj);
636        processObject((Resource)res.getTargetContext(), (Base) res.getTarget(), obj, field.getSelectionSet(), inheritedList, suffix);
637      }
638    }
639    else if (!hasArgument(field.getArguments(), "optional", "true"))
640      throw new EGraphQLException("Unable to resolve reference to "+ref.getReference());
641  }
642
643  private ArgumentListStatus listStatus(Field field, boolean isList) {
644    if (field.hasDirective("singleton"))
645      return ArgumentListStatus.SINGLETON;
646    else if (isList)
647      return ArgumentListStatus.REPEATING;
648    else
649      return ArgumentListStatus.NOT_SPECIFIED;
650  }
651
652  private void processReverseReferenceList(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
653    if (services == null)
654      throw new EGraphQLException("Resource Referencing services not provided");
655    List<IBaseResource> list = new ArrayList<>();
656    List<Argument> params = new ArrayList<>();
657    Argument parg = null;
658    for (Argument a : field.getArguments())
659      if (!(a.getName().equals("_reference")))
660        params.add(a);
661      else if ((parg == null))
662        parg = a;
663      else
664        throw new EGraphQLException("Duplicate parameter _reference");
665    if (parg == null)
666      throw new EGraphQLException("Missing parameter _reference");
667    Argument arg = new Argument();
668    params.add(arg);
669    arg.setName(getSingleValue(parg));
670    arg.addValue(new StringValue(source.fhirType()+"/"+source.getIdPart()));
671    services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), params, list);
672    arg = null;
673    ObjectValue obj = null;
674
675    List<Resource> vl = filterResources(field.argument("fhirpath"), list);
676    if (!vl.isEmpty()) {
677      arg = target.addField(field.getAlias()+suffix, listStatus(field, true));
678      for (Resource v : vl) {
679        obj = new ObjectValue();
680        arg.addValue(obj);
681        processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix);
682      }
683    }
684  }
685  
686  private void processReverseReferenceSearch(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
687    if (services == null)
688      throw new EGraphQLException("Resource Referencing services not provided");
689    List<Argument> params = new ArrayList<Argument>();
690    Argument parg = null;
691    for (Argument a : field.getArguments())
692      if (!(a.getName().equals("_reference")))
693        params.add(a);
694      else if ((parg == null))
695        parg = a;
696      else
697        throw new EGraphQLException("Duplicate parameter _reference");
698    if (parg == null)
699      throw new EGraphQLException("Missing parameter _reference");
700    Argument arg = new Argument();
701    params.add(arg);
702    arg.setName(getSingleValue(parg));
703    arg.addValue(new StringValue(source.fhirType()+"/"+source.getId()));
704    Bundle bnd = (Bundle) services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params);
705    Base bndWrapper = new SearchWrapper(field.getName(), bnd);
706    arg = target.addField(field.getAlias()+suffix, listStatus(field, false));
707    ObjectValue obj = new ObjectValue();
708    arg.addValue(obj);
709    processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix);
710  }
711
712  private void processSearch(ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
713    for (Selection sel : selection) {
714      if ((sel.getField() == null))
715        throw new EGraphQLException("Only field selections are allowed in this context");
716      checkNoDirectives(sel.getField().getDirectives());
717
718      if ((isResourceName(sel.getField().getName(), "")))
719        processSearchSingle(target, sel.getField(), inheritedList, suffix);
720      else if ((isResourceName(sel.getField().getName(), "List")))
721        processSearchSimple(target, sel.getField(), inheritedList, suffix);
722      else if ((isResourceName(sel.getField().getName(), "Connection")))
723        processSearchFull(target, sel.getField(), inheritedList, suffix);
724    }
725  }
726
727  private void processSearchSingle(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
728    if (services == null)
729      throw new EGraphQLException("Resource Referencing services not provided");
730    String id = "";
731    for (Argument arg : field.getArguments())
732      if ((arg.getName().equals("id")))
733        id = getSingleValue(arg);
734      else
735        throw new EGraphQLException("Unknown/invalid parameter "+arg.getName());
736    if (Utilities.noString(id))
737      throw new EGraphQLException("No id found");
738    Resource res = (Resource) services.lookup(appInfo, field.getName(), id);
739    if (res == null)
740      throw new EGraphQLException("Resource "+field.getName()+"/"+id+" not found");
741    Argument arg = target.addField(field.getAlias()+suffix, listStatus(field, false));
742    ObjectValue obj = new ObjectValue();
743    arg.addValue(obj);
744    processObject(res, res, obj, field.getSelectionSet(), inheritedList, suffix);
745  }
746
747  private void processSearchSimple(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
748    if (services == null)
749      throw new EGraphQLException("Resource Referencing services not provided");
750    List<IBaseResource> list = new ArrayList<>();
751    services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), field.getArguments(), list);
752    Argument arg = null;
753    ObjectValue obj = null;
754
755    List<Resource> vl = filterResources(field.argument("fhirpath"), list);
756    if (!vl.isEmpty()) {
757      arg = target.addField(field.getAlias()+suffix, listStatus(field, true));
758      for (Resource v : vl) {
759        obj = new ObjectValue();
760        arg.addValue(obj);
761        processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix);
762      }
763    }
764  }
765  
766  private void processSearchFull(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
767    if (services == null)
768      throw new EGraphQLException("Resource Referencing services not provided");
769    List<Argument> params = new ArrayList<Argument>();
770    Argument carg = null;
771    for ( Argument arg : field.getArguments())
772      if (arg.getName().equals("cursor"))
773        carg = arg;
774      else
775        params.add(arg);
776    if ((carg != null)) {
777      params.clear();;
778      String[] parts = getSingleValue(carg).split(":");
779      params.add(new Argument("search-id", new StringValue(parts[0])));
780      params.add(new Argument("search-offset", new StringValue(parts[1])));
781    }
782
783    Bundle bnd = (Bundle) services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params);
784    SearchWrapper bndWrapper = new SearchWrapper(field.getName(), bnd);
785    Argument arg = target.addField(field.getAlias()+suffix, listStatus(field, false));
786    ObjectValue obj = new ObjectValue();
787    arg.addValue(obj);
788    processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix);
789  }
790
791  private String getSingleValue(Argument arg) throws EGraphQLException {
792    List<Value> vl = resolveValues(arg, 1);
793    if (vl.size() == 0)
794      return "";
795    return vl.get(0).toString();
796  }
797
798  private List<Value> resolveValues(Argument arg) throws EGraphQLException {
799    return resolveValues(arg, -1, "");
800  }
801  
802  private List<Value> resolveValues(Argument arg, int max) throws EGraphQLException {
803    return resolveValues(arg, max, "");
804  }
805  
806  private List<Value> resolveValues(Argument arg, int max, String vars) throws EGraphQLException {
807    List<Value> result = new ArrayList<Value>();
808    for (Value v : arg.getValues()) {
809      if (! (v instanceof VariableValue))
810        result.add(v);
811      else {
812        if (vars.contains(":"+v.toString()+":"))
813          throw new EGraphQLException("Recursive reference to variable "+v.toString());
814        Argument a = workingVariables.get(v.toString());
815        if (a == null)
816          throw new EGraphQLException("No value found for variable \""+v.toString()+"\" in \""+arg.getName()+"\"");
817        List<Value> vl = resolveValues(a, -1, vars+":"+v.toString()+":");
818        result.addAll(vl);
819      }
820    }
821    if ((max != -1 && result.size() > max))
822      throw new EGraphQLException("Only "+Integer.toString(max)+" values are allowed for \""+arg.getName()+"\", but "+Integer.toString(result.size())+" enoucntered");
823    return result;
824  }
825
826
827  
828  
829  public Object getAppInfo() {
830    return appInfo;
831  }
832
833  public void setAppInfo(Object appInfo) {
834    this.appInfo = appInfo;
835  }
836
837  public Resource getFocus() {
838    return focus;
839  }
840
841  @Override
842  public void setFocus(IBaseResource focus) {
843    this.focus = (Resource) focus;
844  }
845
846  public Package getGraphQL() {
847    return graphQL;
848  }
849
850  @Override
851  public void setGraphQL(Package graphQL) {
852    this.graphQL = graphQL;
853  }
854
855  public GraphQLResponse getOutput() {
856    return output;
857  }
858
859  public IGraphQLStorageServices getServices() {
860    return services;
861  }
862
863  @Override
864  public void setServices(IGraphQLStorageServices services) {
865    this.services = services;
866  }
867
868
869  //
870//{ GraphQLSearchWrapper }
871//
872//constructor GraphQLSearchWrapper.Create(bundle : Bundle);
873//var
874//  s : String;
875//{
876//  inherited Create;
877//  FBundle = bundle;
878//  s = bundle_List.Matches["self"];
879//  FParseMap = TParseMap.create(s.Substring(s.IndexOf("?")+1));
880//}
881//
882//destructor GraphQLSearchWrapper.Destroy;
883//{
884//  FParseMap.free;
885//  FBundle.Free;
886//  inherited;
887//}
888//
889//function GraphQLSearchWrapper.extractLink(name: String): String;
890//var
891//  s : String;
892//  pm : TParseMap;
893//{
894//  s = FBundle_List.Matches[name];
895//  if (s == "")
896//    result = null
897//  else
898//  {
899//    pm = TParseMap.create(s.Substring(s.IndexOf("?")+1));
900//    try
901//      result = String.Create(pm.GetVar("search-id")+":"+pm.GetVar("search-offset"));
902//    finally
903//      pm.Free;
904//    }
905//  }
906//}
907//
908//function GraphQLSearchWrapper.extractParam(name: String; int : boolean): Base;
909//var
910//  s : String;
911//{
912//  s = FParseMap.GetVar(name);
913//  if (s == "")
914//    result = null
915//  else if (int)
916//    result = Integer.Create(s)
917//  else
918//    result = String.Create(s);
919//}
920//
921//function GraphQLSearchWrapper.fhirType(): String;
922//{
923//  result = "*Connection";
924//}
925//
926//  // http://test.fhir.org/r4/Patient?_format==text/xhtml&search-id==77c97e03-8a6c-415f-a63d-11c80cf73f&&active==true&_sort==_id&search-offset==50&_count==50
927//
928//function GraphQLSearchWrapper.getPropertyValue(propName: string): Property;
929//var
930//  list : List<GraphQLSearchEdge>;
931//  be : BundleEntry;
932//{
933//  if (propName == "first")
934//    result = Property.Create(self, propname, "string", false, String, extractLink("first"))
935//  else if (propName == "previous")
936//    result = Property.Create(self, propname, "string", false, String, extractLink("previous"))
937//  else if (propName == "next")
938//    result = Property.Create(self, propname, "string", false, String, extractLink("next"))
939//  else if (propName == "last")
940//    result = Property.Create(self, propname, "string", false, String, extractLink("last"))
941//  else if (propName == "count")
942//    result = Property.Create(self, propname, "integer", false, String, FBundle.totalElement)
943//  else if (propName == "offset")
944//    result = Property.Create(self, propname, "integer", false, Integer, extractParam("search-offset", true))
945//  else if (propName == "pagesize")
946//    result = Property.Create(self, propname, "integer", false, Integer, extractParam("_count", true))
947//  else if (propName == "edges")
948//  {
949//    list = ArrayList<GraphQLSearchEdge>();
950//    try
951//      for be in FBundle.getEntry() do
952//        list.add(GraphQLSearchEdge.create(be));
953//      result = Property.Create(self, propname, "integer", true, Integer, List<Base>(list));
954//    finally
955//      list.Free;
956//    }
957//  }
958//  else
959//    result = null;
960//}
961//
962//private void GraphQLSearchWrapper.SetBundle(const Value: Bundle);
963//{
964//  FBundle.Free;
965//  FBundle = Value;
966//}
967//
968//{ GraphQLSearchEdge }
969//
970//constructor GraphQLSearchEdge.Create(entry: BundleEntry);
971//{
972//  inherited Create;
973//  FEntry = entry;
974//}
975//
976//destructor GraphQLSearchEdge.Destroy;
977//{
978//  FEntry.Free;
979//  inherited;
980//}
981//
982//function GraphQLSearchEdge.fhirType(): String;
983//{
984//  result = "*Edge";
985//}
986//
987//function GraphQLSearchEdge.getPropertyValue(propName: string): Property;
988//{
989//  if (propName == "mode")
990//  {
991//    if (FEntry.search != null)
992//      result = Property.Create(self, propname, "code", false, Enum, FEntry.search.modeElement)
993//    else
994//      result = Property.Create(self, propname, "code", false, Enum, Base(null));
995//  }
996//  else if (propName == "score")
997//  {
998//    if (FEntry.search != null)
999//      result = Property.Create(self, propname, "decimal", false, Decimal, FEntry.search.scoreElement)
1000//    else
1001//      result = Property.Create(self, propname, "decimal", false, Decimal, Base(null));
1002//  }
1003//  else if (propName == "resource")
1004//    result = Property.Create(self, propname, "resource", false, Resource, FEntry.getResource())
1005//  else
1006//    result = null;
1007//}
1008//
1009//private void GraphQLSearchEdge.SetEntry(const Value: BundleEntry);
1010//{
1011//  FEntry.Free;
1012//  FEntry = value;
1013//}
1014//
1015}