001package org.hl7.fhir.r4.utils;
002
003import java.io.UnsupportedEncodingException;
004import java.net.URLDecoder;
005import java.util.ArrayList;
006import java.util.HashMap;
007import java.util.LinkedHashMap;
008import java.util.LinkedList;
009import java.util.List;
010import java.util.Map;
011
012import org.hl7.fhir.utilities.graphql.Package;
013import org.hl7.fhir.utilities.graphql.Selection;
014import org.hl7.fhir.utilities.graphql.StringValue;
015import org.hl7.fhir.utilities.graphql.Value;
016import org.hl7.fhir.utilities.graphql.Variable;
017import org.hl7.fhir.utilities.graphql.VariableValue;
018import org.hl7.fhir.exceptions.FHIRException;
019import org.hl7.fhir.r4.context.IWorkerContext;
020import org.hl7.fhir.r4.model.BackboneElement;
021import org.hl7.fhir.r4.model.Base;
022import org.hl7.fhir.r4.model.Bundle;
023import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
024import org.hl7.fhir.r4.model.Bundle.BundleLinkComponent;
025import org.hl7.fhir.r4.model.CanonicalType;
026import org.hl7.fhir.r4.model.DomainResource;
027import org.hl7.fhir.r4.model.Element;
028import org.hl7.fhir.r4.model.ExpressionNode;
029import org.hl7.fhir.r4.model.IntegerType;
030import org.hl7.fhir.r4.model.Property;
031import org.hl7.fhir.r4.model.Reference;
032import org.hl7.fhir.r4.model.Resource;
033import org.hl7.fhir.r4.model.StringType;
034import org.hl7.fhir.r4.model.StructureDefinition;
035import org.hl7.fhir.r4.utils.GraphQLEngine.SearchEdge;
036import org.hl7.fhir.r4.utils.GraphQLEngine.SearchWrapper;
037import org.hl7.fhir.r4.utils.FHIRLexer.FHIRLexerException;
038import org.hl7.fhir.r4.utils.GraphQLEngine.IGraphQLStorageServices.ReferenceResolution;
039import org.hl7.fhir.utilities.Utilities;
040import org.hl7.fhir.utilities.graphql.Argument;
041import org.hl7.fhir.utilities.graphql.Argument.ArgumentListStatus;
042import org.hl7.fhir.utilities.graphql.Directive;
043import org.hl7.fhir.utilities.graphql.EGraphEngine;
044import org.hl7.fhir.utilities.graphql.EGraphQLException;
045import org.hl7.fhir.utilities.graphql.Field;
046import org.hl7.fhir.utilities.graphql.Fragment;
047import org.hl7.fhir.utilities.graphql.NameValue;
048import org.hl7.fhir.utilities.graphql.NumberValue;
049import org.hl7.fhir.utilities.graphql.ObjectValue;
050import org.hl7.fhir.utilities.graphql.Operation;
051import org.hl7.fhir.utilities.graphql.Operation.OperationType;
052
053public class GraphQLEngine {
054  
055  public class SearchEdge extends Base {
056
057    private BundleEntryComponent be;
058    private String type;
059    
060    public SearchEdge(String type, BundleEntryComponent be) {
061      this.type = type;
062      this.be = be;
063    }
064    @Override
065    public String fhirType() {
066      return type;
067    }
068
069    @Override
070    protected void listChildren(List<Property> result) {
071      throw new Error("Not Implemented");
072    }
073
074    @Override
075    public String getIdBase() {
076      throw new Error("Not Implemented");
077    }
078
079    @Override
080    public void setIdBase(String value) {
081      throw new Error("Not Implemented");
082    }
083
084    @Override
085    public Property getNamedProperty(int _hash, String _name, boolean _checkValid) throws FHIRException {
086      switch (_hash) {
087      case 3357091:    /*mode*/     return new Property(_name, "string",   "n/a", 0, 1, be.getSearch().hasMode() ? be.getSearch().getModeElement() : null);
088      case 109264530:  /*score*/    return new Property(_name, "string",   "n/a", 0, 1, be.getSearch().hasScore() ? be.getSearch().getScoreElement() : null);
089      case -341064690: /*resource*/ return new Property(_name, "resource",  "n/a", 0, 1, be.hasResource() ? be.getResource() : null);
090      default: return super.getNamedProperty(_hash, _name, _checkValid);
091      }
092    }
093  }
094
095  public class SearchWrapper extends Base {
096
097    private Bundle bnd;
098    private String type;
099    private Map<String, String> map;
100
101    public SearchWrapper(String type, Bundle bnd) throws FHIRException {
102      this.type = type;
103      this.bnd = bnd;
104      for (BundleLinkComponent bl : bnd.getLink()) 
105        if (bl.getRelation().equals("self"))
106          map = parseURL(bl.getUrl());
107    }
108
109    @Override
110    public String fhirType() {
111      return type;
112    }
113
114    @Override
115    protected void listChildren(List<Property> result) {
116      throw new Error("Not Implemented");
117    }
118
119    @Override
120    public String getIdBase() {
121      throw new Error("Not Implemented");
122    }
123
124    @Override
125    public void setIdBase(String value) {
126      throw new Error("Not Implemented");
127    }
128
129    @Override
130    public Property getNamedProperty(int _hash, String _name, boolean _checkValid) throws FHIRException {
131      switch (_hash) {
132      case 97440432:   /*first*/     return new Property(_name, "string",  "n/a", 0, 1, extractLink(_name));
133      case -1273775369: /*previous*/  return new Property(_name, "string",  "n/a", 0, 1, extractLink(_name));
134      case 3377907:    /*next*/      return new Property(_name, "string",  "n/a", 0, 1, extractLink(_name));
135      case 3314326:    /*last*/      return new Property(_name, "string",  "n/a", 0, 1, extractLink(_name));
136      case 94851343:   /*count*/     return new Property(_name, "integer", "n/a", 0, 1, bnd.getTotalElement());
137      case -1019779949:/*offset*/    return new Property(_name, "integer", "n/a", 0, 1, extractParam("search-offset"));
138      case 860381968:  /*pagesize*/  return new Property(_name, "integer", "n/a", 0, 1, extractParam("_count"));
139      case 96356950:  /*edges*/      return new Property(_name, "edge",    "n/a", 0, Integer.MAX_VALUE, getEdges());
140      default: return super.getNamedProperty(_hash, _name, _checkValid);
141      }
142    }
143
144    private List<Base> getEdges() {
145      List<Base> list = new ArrayList<>();
146      for (BundleEntryComponent be : bnd.getEntry())
147        list.add(new SearchEdge(type.substring(0, type.length()-10)+"Edge", be));
148      return list;
149    }
150
151    private Base extractParam(String name) throws FHIRException {
152      return map != null ? new IntegerType(map.get(name)) : null;
153    }
154
155    private Map<String, String> parseURL(String url) throws FHIRException {
156      try {
157        Map<String, String> map = new HashMap<String, String>();
158        String[] pairs = url.split("&");
159        for (String pair : pairs) {
160          int idx = pair.indexOf("=");
161          String key;
162          key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair;
163          String value = idx > 0 && pair.length() > idx + 1 ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null;
164          map.put(key, value);
165        }
166        return map;
167      } catch (UnsupportedEncodingException e) {
168        throw new FHIRException(e);
169      }
170    }
171
172    private Base extractLink(String _name) throws FHIRException {
173      for (BundleLinkComponent bl : bnd.getLink()) {
174        if (bl.getRelation().equals(_name)) {
175          Map<String, String> map = parseURL(bl.getUrl());
176          return new StringType(map.get("search-id")+':'+map.get("search-offset"));
177        }
178      }
179      return null;
180    }
181
182  }
183
184  public interface IGraphQLStorageServices {
185    public class ReferenceResolution {
186      private Resource targetContext;
187      private Resource target;
188      public ReferenceResolution(Resource targetContext, Resource target) {
189        super();
190        this.targetContext = targetContext;
191        this.target = target;
192      }
193      
194      
195    }
196    // given a reference inside a context, return what it references (including resolving internal references (e.g. start with #)
197    public ReferenceResolution lookup(Object appInfo, Resource context, Reference reference) throws FHIRException;
198    
199    // just get the identified resource
200    public Resource lookup(Object appInfo, String type, String id) throws FHIRException;
201
202    // list the matching resources. searchParams are the standard search params. 
203    // this instanceof different to search because the server returns all matching resources, or an error. There instanceof no paging on this search   
204    public void listResources(Object appInfo, String type, List<Argument> searchParams, List<Resource> matches) throws FHIRException;
205
206    // just perform a standard search, and return the bundle as you return to the client  
207    public Bundle search(Object appInfo, String type, List<Argument> searchParams) throws FHIRException;
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 ObjectValue 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 ObjectValue();
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, Resource 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, 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      int i = 0;
381      int t = 0;
382      if (fp.length() == 0)
383        for (Base v : values) {
384          if ((i >= offset) && passesExtensionMode(v, extensionMode)) {
385            result.add(v);
386            t++;
387            if (t >= count)
388              break;
389          }
390          i++;
391        } else {
392          ExpressionNode node = fpe.parse(fp.toString().substring(5));
393          for (Base v : values) {
394            if ((i >= offset) && passesExtensionMode(v, extensionMode) && fpe.evaluateToBoolean(null, context, v, node)) {
395              result.add(v);
396              t++;
397              if (t >= count)
398                break;
399            }
400            i++;
401          }
402        }
403    }
404    return result;
405  }
406
407  private List<Resource> filterResources(Argument fhirpath, Bundle bnd) throws EGraphQLException, FHIRException {
408    List<Resource> result = new ArrayList<Resource>();
409    if (bnd.getEntry().size() > 0) {
410      if ((fhirpath == null))
411        for (BundleEntryComponent be : bnd.getEntry())
412          result.add(be.getResource());
413      else {
414        FHIRPathEngine fpe = new FHIRPathEngine(context);
415        ExpressionNode node = fpe.parse(getSingleValue(fhirpath));
416        for (BundleEntryComponent be : bnd.getEntry())
417          if (fpe.evaluateToBoolean(null, be.getResource(), be.getResource(), node))
418            result.add(be.getResource());
419      }
420    }
421    return result;
422  }
423
424  private List<Resource> filterResources(Argument fhirpath, List<Resource> list) throws EGraphQLException, FHIRException {
425    List<Resource> result = new ArrayList<Resource>();
426    if (list.size() > 0) {
427      if ((fhirpath == null))
428        for (Resource v : list)
429          result.add(v);
430      else {
431        FHIRPathEngine fpe = new FHIRPathEngine(context);
432        ExpressionNode node = fpe.parse(getSingleValue(fhirpath));
433        for (Resource v : list)
434          if (fpe.evaluateToBoolean(null, v, v, node))
435            result.add(v);
436      }
437    }
438    return result;
439  }
440
441  private boolean hasArgument(List<Argument> arguments, String name, String value) {
442    for (Argument arg : arguments)
443      if ((arg.getName().equals(name)) && arg.hasValue(value))
444        return true;
445    return false;
446  }
447
448  private void processValues(Resource context, Selection sel, Property prop, ObjectValue target, List<Base> values, boolean extensionMode, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
449    boolean il = false;
450    Argument arg = null;
451    ExpressionNode expression = null;
452    if (sel.getField().hasDirective("slice")) {
453      Directive dir = sel.getField().directive("slice");
454      String s = ((StringValue) dir.getArguments().get(0).getValues().get(0)).getValue();
455      if (s.equals("$index"))
456        expression = magicExpression;
457      else
458        expression = fpe.parse(s);
459    }
460    if (sel.getField().hasDirective("flatten")) // special: instruction to drop this node...
461      il = prop.isList() && !sel.getField().hasDirective("first");
462    else if (sel.getField().hasDirective("first")) {
463      if (expression != null) 
464        throw new FHIRException("You cannot mix @slice and @first");
465      arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), inheritedList));
466    } else if (expression == null)
467      arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), prop.isList() || inheritedList));
468
469    
470    int index = 0;
471    for (Base value : values) {
472      String ss = "";
473      if (expression != null) {
474        if (expression == magicExpression)
475          ss = suffix+'.'+Integer.toString(index);
476        else
477          ss = suffix+'.'+fpe.evaluateToString(null, null, value, expression);
478        if (!sel.getField().hasDirective("flatten"))
479          arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), prop.isList() || inheritedList));
480      }
481
482      if (value.isPrimitive() && !extensionMode) {
483        if (!sel.getField().getSelectionSet().isEmpty())
484          throw new EGraphQLException("Encountered a selection set on a scalar field type");
485        processPrimitive(arg, value);
486      } else {
487        if (sel.getField().getSelectionSet().isEmpty())
488          throw new EGraphQLException("No Fields selected on a complex object");
489        if (arg == null)
490          processObject(context, value, target, sel.getField().getSelectionSet(), il, ss);
491        else {
492          ObjectValue n = new ObjectValue();
493          arg.addValue(n);
494          processObject(context, value, n, sel.getField().getSelectionSet(), il, ss);
495        }
496      }
497      if (sel.getField().hasDirective("first"))
498        return;
499      index++;
500    }
501  }
502
503  private void processVariables(Operation op) throws EGraphQLException {
504    for (Variable varRef : op.getVariables()) {
505      Argument varDef = null;
506      for (Argument v : graphQL.getVariables()) 
507        if (v.getName().equals(varRef.getName()))
508          varDef = v;
509      if (varDef != null)
510        workingVariables.put(varRef.getName(), varDef); // todo: check type?
511      else if (varRef.getDefaultValue() != null)
512        workingVariables.put(varRef.getName(), new Argument(varRef.getName(), varRef.getDefaultValue()));
513      else
514        throw new EGraphQLException("No value found for variable ");
515    }
516  }
517
518  private boolean isPrimitive(String typename) {
519    return Utilities.existsInList(typename, "boolean", "integer", "string", "decimal", "uri", "base64Binary", "instant", "date", "dateTime", "time", "code", "oid", "id", "markdown", "unsignedInt", "positiveInt", "url", "canonical");
520  }
521
522  private boolean isResourceName(String name, String suffix) {
523    if (!name.endsWith(suffix))
524      return false;
525    name = name.substring(0, name.length()-suffix.length());
526    return context.getResourceNamesAsSet().contains(name);
527  }
528
529  private void processObject(Resource context, Base source, ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
530    for (Selection sel : selection) {
531      if (sel.getField() != null) {
532        if (checkDirectives(sel.getField().getDirectives())) {
533          Property prop = source.getNamedProperty(sel.getField().getName());
534          if ((prop == null) && sel.getField().getName().startsWith("_"))
535            prop = source.getNamedProperty(sel.getField().getName().substring(1));
536          if (prop == null) {
537            if ((sel.getField().getName().equals("resourceType") && source instanceof Resource))
538              target.addField("resourceType", listStatus(sel.getField(), false)).addValue(new StringValue(source.fhirType()));
539            else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("Reference")))
540              processReference(context, source, sel.getField(), target, inheritedList, suffix);
541            else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("canonical")))
542              processCanonicalReference(context, source, sel.getField(), target, inheritedList, suffix);
543            else if (isResourceName(sel.getField().getName(), "List") && (source instanceof Resource))
544              processReverseReferenceList((Resource) source, sel.getField(), target, inheritedList, suffix);
545            else if (isResourceName(sel.getField().getName(), "Connection") && (source instanceof Resource))
546              processReverseReferenceSearch((Resource) source, sel.getField(), target, inheritedList, suffix);
547            else
548              throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType());
549          } else {
550            if (!isPrimitive(prop.getTypeCode()) && sel.getField().getName().startsWith("_"))
551              throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType());
552
553            List<Base> vl = filter(context, prop, sel.getField().getArguments(), prop.getValues(), sel.getField().getName().startsWith("_"));
554            if (!vl.isEmpty())
555              processValues(context, sel, prop, target, vl, sel.getField().getName().startsWith("_"), inheritedList, suffix);
556          }
557        }
558      } else if (sel.getInlineFragment() != null) {
559        if (checkDirectives(sel.getInlineFragment().getDirectives())) {
560          if (Utilities.noString(sel.getInlineFragment().getTypeCondition()))
561            throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid?
562          if (source.fhirType().equals(sel.getInlineFragment().getTypeCondition())) 
563            processObject(context, source, target, sel.getInlineFragment().getSelectionSet(), inheritedList, suffix);
564        }
565      } else if (checkDirectives(sel.getFragmentSpread().getDirectives())) {
566        Fragment fragment = graphQL.getDocument().fragment(sel.getFragmentSpread().getName());
567        if (fragment == null)
568          throw new EGraphQLException("Unable to resolve fragment "+sel.getFragmentSpread().getName());
569
570        if (Utilities.noString(fragment.getTypeCondition()))
571          throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid?
572        if (source.fhirType().equals(fragment.getTypeCondition()))
573          processObject(context, source, target, fragment.getSelectionSet(), inheritedList, suffix);
574      }
575    }
576  }
577
578  private void processPrimitive(Argument arg, Base value) {
579    String s = value.fhirType();
580    if (s.equals("integer") || s.equals("decimal") || s.equals("unsignedInt") || s.equals("positiveInt"))
581      arg.addValue(new NumberValue(value.primitiveValue()));
582    else if (s.equals("boolean"))
583      arg.addValue(new NameValue(value.primitiveValue()));
584    else
585      arg.addValue(new StringValue(value.primitiveValue()));
586  }
587
588  private void processReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
589    if (!(source instanceof Reference))
590      throw new EGraphQLException("Not done yet");
591    if (services == null)
592      throw new EGraphQLException("Resource Referencing services not provided");
593
594    Reference ref = (Reference) source;
595    ReferenceResolution res = services.lookup(appInfo, context, ref);
596    if (res != null) {
597      if (targetTypeOk(field.getArguments(), res.target)) {
598        Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList));
599        ObjectValue obj = new ObjectValue();
600        arg.addValue(obj);
601        processObject(res.targetContext, res.target, obj, field.getSelectionSet(), inheritedList, suffix);
602      }
603    }
604    else if (!hasArgument(field.getArguments(), "optional", "true"))
605      throw new EGraphQLException("Unable to resolve reference to "+ref.getReference());
606  }
607
608  private void processCanonicalReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
609    if (!(source instanceof CanonicalType))
610      throw new EGraphQLException("Not done yet");
611    if (services == null)
612      throw new EGraphQLException("Resource Referencing services not provided");
613
614    Reference ref = new Reference(source.primitiveValue());
615    ReferenceResolution res = services.lookup(appInfo, context, ref);
616    if (res != null) {
617      if (targetTypeOk(field.getArguments(), res.target)) {
618        Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList));
619        ObjectValue obj = new ObjectValue();
620        arg.addValue(obj);
621        processObject(res.targetContext, res.target, obj, field.getSelectionSet(), inheritedList, suffix);
622      }
623    }
624    else if (!hasArgument(field.getArguments(), "optional", "true"))
625      throw new EGraphQLException("Unable to resolve reference to "+ref.getReference());
626  }
627
628  private ArgumentListStatus listStatus(Field field, boolean isList) {
629    if (field.hasDirective("singleton"))
630      return ArgumentListStatus.SINGLETON;
631    else if (isList)
632      return ArgumentListStatus.REPEATING;
633    else
634      return ArgumentListStatus.NOT_SPECIFIED;
635  }
636
637  private void processReverseReferenceList(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
638    if (services == null)
639      throw new EGraphQLException("Resource Referencing services not provided");
640    List<Resource> list = new ArrayList<Resource>();
641    List<Argument> params = new ArrayList<Argument>();
642    Argument parg = null;
643    for (Argument a : field.getArguments())
644      if (!(a.getName().equals("_reference")))
645        params.add(a);
646      else if ((parg == null))
647        parg = a;
648      else
649        throw new EGraphQLException("Duplicate parameter _reference");
650    if (parg == null)
651      throw new EGraphQLException("Missing parameter _reference");
652    Argument arg = new Argument();
653    params.add(arg);
654    arg.setName(getSingleValue(parg));
655    arg.addValue(new StringValue(source.fhirType()+"/"+source.getId()));
656    services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), params, list);
657    arg = null;
658    ObjectValue obj = null;
659
660    List<Resource> vl = filterResources(field.argument("fhirpath"), list);
661    if (!vl.isEmpty()) {
662      arg = target.addField(field.getAlias()+suffix, listStatus(field, true));
663      for (Resource v : vl) {
664        obj = new ObjectValue();
665        arg.addValue(obj);
666        processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix);
667      }
668    }
669  }
670  
671  private void processReverseReferenceSearch(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
672    if (services == null)
673      throw new EGraphQLException("Resource Referencing services not provided");
674    List<Argument> params = new ArrayList<Argument>();
675    Argument parg = null;
676    for (Argument a : field.getArguments())
677      if (!(a.getName().equals("_reference")))
678        params.add(a);
679      else if ((parg == null))
680        parg = a;
681      else
682        throw new EGraphQLException("Duplicate parameter _reference");
683    if (parg == null)
684      throw new EGraphQLException("Missing parameter _reference");
685    Argument arg = new Argument();
686    params.add(arg);
687    arg.setName(getSingleValue(parg));
688    arg.addValue(new StringValue(source.fhirType()+"/"+source.getId()));
689    Bundle bnd = services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params);
690    Base bndWrapper = new SearchWrapper(field.getName(), bnd);
691    arg = target.addField(field.getAlias()+suffix, listStatus(field, false));
692    ObjectValue obj = new ObjectValue();
693    arg.addValue(obj);
694    processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix);
695  }
696
697  private void processSearch(ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
698    for (Selection sel : selection) {
699      if ((sel.getField() == null))
700        throw new EGraphQLException("Only field selections are allowed in this context");
701      checkNoDirectives(sel.getField().getDirectives());
702
703      if ((isResourceName(sel.getField().getName(), "")))
704        processSearchSingle(target, sel.getField(), inheritedList, suffix);
705      else if ((isResourceName(sel.getField().getName(), "List")))
706        processSearchSimple(target, sel.getField(), inheritedList, suffix);
707      else if ((isResourceName(sel.getField().getName(), "Connection")))
708        processSearchFull(target, sel.getField(), inheritedList, suffix);
709    }
710  }
711
712  private void processSearchSingle(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
713    if (services == null)
714      throw new EGraphQLException("Resource Referencing services not provided");
715    String id = "";
716    for (Argument arg : field.getArguments())
717      if ((arg.getName().equals("id")))
718        id = getSingleValue(arg);
719      else
720        throw new EGraphQLException("Unknown/invalid parameter "+arg.getName());
721    if (Utilities.noString(id))
722      throw new EGraphQLException("No id found");
723    Resource res = services.lookup(appInfo, field.getName(), id);
724    if (res == null)
725      throw new EGraphQLException("Resource "+field.getName()+"/"+id+" not found");
726    Argument arg = target.addField(field.getAlias()+suffix, listStatus(field, false));
727    ObjectValue obj = new ObjectValue();
728    arg.addValue(obj);
729    processObject(res, res, obj, field.getSelectionSet(), inheritedList, suffix);
730  }
731
732  private void processSearchSimple(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
733    if (services == null)
734      throw new EGraphQLException("Resource Referencing services not provided");
735    List<Resource> list = new ArrayList<Resource>();
736    services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), field.getArguments(), list);
737    Argument arg = null;
738    ObjectValue obj = null;
739
740    List<Resource> vl = filterResources(field.argument("fhirpath"), list);
741    if (!vl.isEmpty()) {
742      arg = target.addField(field.getAlias()+suffix, listStatus(field, true));
743      for (Resource v : vl) {
744        obj = new ObjectValue();
745        arg.addValue(obj);
746        processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix);
747      }
748    }
749  }
750  
751  private void processSearchFull(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
752    if (services == null)
753      throw new EGraphQLException("Resource Referencing services not provided");
754    List<Argument> params = new ArrayList<Argument>();
755    Argument carg = null;
756    for ( Argument arg : field.getArguments())
757      if (arg.getName().equals("cursor"))
758        carg = arg;
759      else
760        params.add(arg);
761    if ((carg != null)) {
762      params.clear();;
763      String[] parts = getSingleValue(carg).split(":");
764      params.add(new Argument("search-id", new StringValue(parts[0])));
765      params.add(new Argument("search-offset", new StringValue(parts[1])));
766    }
767
768    Bundle bnd = services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params);
769    SearchWrapper bndWrapper = new SearchWrapper(field.getName(), bnd);
770    Argument arg = target.addField(field.getAlias()+suffix, listStatus(field, false));
771    ObjectValue obj = new ObjectValue();
772    arg.addValue(obj);
773    processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix);
774  }
775
776  private String getSingleValue(Argument arg) throws EGraphQLException {
777    List<Value> vl = resolveValues(arg, 1);
778    if (vl.size() == 0)
779      return "";
780    return vl.get(0).toString();
781  }
782
783  private List<Value> resolveValues(Argument arg) throws EGraphQLException {
784    return resolveValues(arg, -1, "");
785  }
786  
787  private List<Value> resolveValues(Argument arg, int max) throws EGraphQLException {
788    return resolveValues(arg, max, "");
789  }
790  
791  private List<Value> resolveValues(Argument arg, int max, String vars) throws EGraphQLException {
792    List<Value> result = new ArrayList<Value>();
793    for (Value v : arg.getValues()) {
794      if (! (v instanceof VariableValue))
795        result.add(v);
796      else {
797        if (vars.contains(":"+v.toString()+":"))
798          throw new EGraphQLException("Recursive reference to variable "+v.toString());
799        Argument a = workingVariables.get(v.toString());
800        if (a == null)
801          throw new EGraphQLException("No value found for variable \""+v.toString()+"\" in \""+arg.getName()+"\"");
802        List<Value> vl = resolveValues(a, -1, vars+":"+v.toString()+":");
803        result.addAll(vl);
804      }
805    }
806    if ((max != -1 && result.size() > max))
807      throw new EGraphQLException("Only "+Integer.toString(max)+" values are allowed for \""+arg.getName()+"\", but "+Integer.toString(result.size())+" enoucntered");
808    return result;
809  }
810
811
812  
813  
814  public Object getAppInfo() {
815    return appInfo;
816  }
817
818  public void setAppInfo(Object appInfo) {
819    this.appInfo = appInfo;
820  }
821
822  public Resource getFocus() {
823    return focus;
824  }
825
826  public void setFocus(Resource focus) {
827    this.focus = focus;
828  }
829
830  public Package getGraphQL() {
831    return graphQL;
832  }
833
834  public void setGraphQL(Package graphQL) {
835    this.graphQL = graphQL;
836  }
837
838  public ObjectValue getOutput() {
839    return output;
840  }
841
842  public IGraphQLStorageServices getServices() {
843    return services;
844  }
845
846  public void setServices(IGraphQLStorageServices services) {
847    this.services = services;
848  }
849
850
851  //
852//{ GraphQLSearchWrapper }
853//
854//constructor GraphQLSearchWrapper.Create(bundle : Bundle);
855//var
856//  s : String;
857//{
858//  inherited Create;
859//  FBundle = bundle;
860//  s = bundle_List.Matches["self"];
861//  FParseMap = TParseMap.create(s.Substring(s.IndexOf("?")+1));
862//}
863//
864//destructor GraphQLSearchWrapper.Destroy;
865//{
866//  FParseMap.free;
867//  FBundle.Free;
868//  inherited;
869//}
870//
871//function GraphQLSearchWrapper.extractLink(name: String): String;
872//var
873//  s : String;
874//  pm : TParseMap;
875//{
876//  s = FBundle_List.Matches[name];
877//  if (s == "")
878//    result = null
879//  else
880//  {
881//    pm = TParseMap.create(s.Substring(s.IndexOf("?")+1));
882//    try
883//      result = String.Create(pm.GetVar("search-id")+":"+pm.GetVar("search-offset"));
884//    finally
885//      pm.Free;
886//    }
887//  }
888//}
889//
890//function GraphQLSearchWrapper.extractParam(name: String; int : boolean): Base;
891//var
892//  s : String;
893//{
894//  s = FParseMap.GetVar(name);
895//  if (s == "")
896//    result = null
897//  else if (int)
898//    result = Integer.Create(s)
899//  else
900//    result = String.Create(s);
901//}
902//
903//function GraphQLSearchWrapper.fhirType(): String;
904//{
905//  result = "*Connection";
906//}
907//
908//  // 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
909//
910//function GraphQLSearchWrapper.getPropertyValue(propName: string): Property;
911//var
912//  list : List<GraphQLSearchEdge>;
913//  be : BundleEntry;
914//{
915//  if (propName == "first")
916//    result = Property.Create(self, propname, "string", false, String, extractLink("first"))
917//  else if (propName == "previous")
918//    result = Property.Create(self, propname, "string", false, String, extractLink("previous"))
919//  else if (propName == "next")
920//    result = Property.Create(self, propname, "string", false, String, extractLink("next"))
921//  else if (propName == "last")
922//    result = Property.Create(self, propname, "string", false, String, extractLink("last"))
923//  else if (propName == "count")
924//    result = Property.Create(self, propname, "integer", false, String, FBundle.totalElement)
925//  else if (propName == "offset")
926//    result = Property.Create(self, propname, "integer", false, Integer, extractParam("search-offset", true))
927//  else if (propName == "pagesize")
928//    result = Property.Create(self, propname, "integer", false, Integer, extractParam("_count", true))
929//  else if (propName == "edges")
930//  {
931//    list = ArrayList<GraphQLSearchEdge>();
932//    try
933//      for be in FBundle.getEntry() do
934//        list.add(GraphQLSearchEdge.create(be));
935//      result = Property.Create(self, propname, "integer", true, Integer, List<Base>(list));
936//    finally
937//      list.Free;
938//    }
939//  }
940//  else
941//    result = null;
942//}
943//
944//private void GraphQLSearchWrapper.SetBundle(const Value: Bundle);
945//{
946//  FBundle.Free;
947//  FBundle = Value;
948//}
949//
950//{ GraphQLSearchEdge }
951//
952//constructor GraphQLSearchEdge.Create(entry: BundleEntry);
953//{
954//  inherited Create;
955//  FEntry = entry;
956//}
957//
958//destructor GraphQLSearchEdge.Destroy;
959//{
960//  FEntry.Free;
961//  inherited;
962//}
963//
964//function GraphQLSearchEdge.fhirType(): String;
965//{
966//  result = "*Edge";
967//}
968//
969//function GraphQLSearchEdge.getPropertyValue(propName: string): Property;
970//{
971//  if (propName == "mode")
972//  {
973//    if (FEntry.search != null)
974//      result = Property.Create(self, propname, "code", false, Enum, FEntry.search.modeElement)
975//    else
976//      result = Property.Create(self, propname, "code", false, Enum, Base(null));
977//  }
978//  else if (propName == "score")
979//  {
980//    if (FEntry.search != null)
981//      result = Property.Create(self, propname, "decimal", false, Decimal, FEntry.search.scoreElement)
982//    else
983//      result = Property.Create(self, propname, "decimal", false, Decimal, Base(null));
984//  }
985//  else if (propName == "resource")
986//    result = Property.Create(self, propname, "resource", false, Resource, FEntry.getResource())
987//  else
988//    result = null;
989//}
990//
991//private void GraphQLSearchEdge.SetEntry(const Value: BundleEntry);
992//{
993//  FEntry.Free;
994//  FEntry = value;
995//}
996//
997}