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