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