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, 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      int i = 0;
385      int t = 0;
386      if (fp.length() == 0)
387        for (Base v : values) {
388          if ((i >= offset) && passesExtensionMode(v, extensionMode)) {
389            result.add(v);
390            t++;
391            if (t >= count)
392              break;
393          }
394          i++;
395        } else {
396          ExpressionNode node = fpe.parse(fp.toString().substring(5));
397          for (Base v : values) {
398            if ((i >= offset) && passesExtensionMode(v, extensionMode) && fpe.evaluateToBoolean(null, context, v, node)) {
399              result.add(v);
400              t++;
401              if (t >= count)
402                break;
403            }
404            i++;
405          }
406        }
407    }
408    return result;
409  }
410
411  private List<Resource> filterResources(Argument fhirpath, Bundle bnd) throws EGraphQLException, FHIRException {
412    List<Resource> result = new ArrayList<Resource>();
413    if (bnd.getEntry().size() > 0) {
414      if ((fhirpath == null))
415        for (BundleEntryComponent be : bnd.getEntry())
416          result.add(be.getResource());
417      else {
418        FHIRPathEngine fpe = new FHIRPathEngine(context);
419        ExpressionNode node = fpe.parse(getSingleValue(fhirpath));
420        for (BundleEntryComponent be : bnd.getEntry())
421          if (fpe.evaluateToBoolean(null, be.getResource(), be.getResource(), node))
422            result.add(be.getResource());
423      }
424    }
425    return result;
426  }
427
428  private List<Resource> filterResources(Argument fhirpath, List<IBaseResource> list) throws EGraphQLException, FHIRException {
429    List<Resource> result = new ArrayList<Resource>();
430    if (list.size() > 0) {
431      if ((fhirpath == null))
432        for (IBaseResource v : list)
433          result.add((Resource) v);
434      else {
435        FHIRPathEngine fpe = new FHIRPathEngine(context);
436        ExpressionNode node = fpe.parse(getSingleValue(fhirpath));
437        for (IBaseResource v : list)
438          if (fpe.evaluateToBoolean(null, (Resource)v, (Base) v, node))
439            result.add((Resource) v);
440      }
441    }
442    return result;
443  }
444
445  private boolean hasArgument(List<Argument> arguments, String name, String value) {
446    for (Argument arg : arguments)
447      if ((arg.getName().equals(name)) && arg.hasValue(value))
448        return true;
449    return false;
450  }
451
452  private void processValues(Resource context, Selection sel, Property prop, ObjectValue target, List<Base> values, boolean extensionMode, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
453    boolean il = false;
454    Argument arg = null;
455    ExpressionNode expression = null;
456    if (sel.getField().hasDirective("slice")) {
457      Directive dir = sel.getField().directive("slice");
458      String s = ((StringValue) dir.getArguments().get(0).getValues().get(0)).getValue();
459      if (s.equals("$index"))
460        expression = magicExpression;
461      else
462        expression = fpe.parse(s);
463    }
464    if (sel.getField().hasDirective("flatten")) // special: instruction to drop this node...
465      il = prop.isList() && !sel.getField().hasDirective("first");
466    else if (sel.getField().hasDirective("first")) {
467      if (expression != null) 
468        throw new FHIRException("You cannot mix @slice and @first");
469      arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), inheritedList));
470    } else if (expression == null)
471      arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), prop.isList() || inheritedList));
472
473    
474    int index = 0;
475    for (Base value : values) {
476      String ss = "";
477      if (expression != null) {
478        if (expression == magicExpression)
479          ss = suffix+'.'+Integer.toString(index);
480        else
481          ss = suffix+'.'+fpe.evaluateToString(null, null, null, value, expression);
482        if (!sel.getField().hasDirective("flatten"))
483          arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), prop.isList() || inheritedList));
484      }
485
486      if (value.isPrimitive() && !extensionMode) {
487        if (!sel.getField().getSelectionSet().isEmpty())
488          throw new EGraphQLException("Encountered a selection set on a scalar field type");
489        processPrimitive(arg, value);
490      } else {
491        if (sel.getField().getSelectionSet().isEmpty())
492          throw new EGraphQLException("No Fields selected on a complex object");
493        if (arg == null)
494          processObject(context, value, target, sel.getField().getSelectionSet(), il, ss);
495        else {
496          ObjectValue n = new ObjectValue();
497          arg.addValue(n);
498          processObject(context, value, n, sel.getField().getSelectionSet(), il, ss);
499        }
500      }
501      if (sel.getField().hasDirective("first"))
502        return;
503      index++;
504    }
505  }
506
507  private void processVariables(Operation op) throws EGraphQLException {
508    for (Variable varRef : op.getVariables()) {
509      Argument varDef = null;
510      for (Argument v : graphQL.getVariables()) 
511        if (v.getName().equals(varRef.getName()))
512          varDef = v;
513      if (varDef != null)
514        workingVariables.put(varRef.getName(), varDef); // todo: check type?
515      else if (varRef.getDefaultValue() != null)
516        workingVariables.put(varRef.getName(), new Argument(varRef.getName(), varRef.getDefaultValue()));
517      else
518        throw new EGraphQLException("No value found for variable ");
519    }
520  }
521
522  private boolean isPrimitive(String typename) {
523    return Utilities.existsInList(typename, "boolean", "integer", "string", "decimal", "uri", "base64Binary", "instant", "date", "dateTime", "time", "code", "oid", "id", "markdown", "unsignedInt", "positiveInt", "url", "canonical");
524  }
525
526  private boolean isResourceName(String name, String suffix) {
527    if (!name.endsWith(suffix))
528      return false;
529    name = name.substring(0, name.length()-suffix.length());
530    return context.getResourceNamesAsSet().contains(name);
531  }
532
533  private void processObject(Resource context, Base source, ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
534    for (Selection sel : selection) {
535      if (sel.getField() != null) {
536        if (checkDirectives(sel.getField().getDirectives())) {
537          Property prop = source.getNamedProperty(sel.getField().getName());
538          if ((prop == null) && sel.getField().getName().startsWith("_"))
539            prop = source.getNamedProperty(sel.getField().getName().substring(1));
540          if (prop == null) {
541            if ((sel.getField().getName().equals("resourceType") && source instanceof Resource))
542              target.addField("resourceType", listStatus(sel.getField(), false)).addValue(new StringValue(source.fhirType()));
543            else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("Reference")))
544              processReference(context, source, sel.getField(), target, inheritedList, suffix);
545            else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("canonical")))
546              processCanonicalReference(context, source, sel.getField(), target, inheritedList, suffix);
547            else if (isResourceName(sel.getField().getName(), "List") && (source instanceof Resource))
548              processReverseReferenceList((Resource) source, sel.getField(), target, inheritedList, suffix);
549            else if (isResourceName(sel.getField().getName(), "Connection") && (source instanceof Resource))
550              processReverseReferenceSearch((Resource) source, sel.getField(), target, inheritedList, suffix);
551            else
552              throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType());
553          } else {
554            if (!isPrimitive(prop.getTypeCode()) && sel.getField().getName().startsWith("_"))
555              throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType());
556
557            List<Base> vl = filter(context, prop, sel.getField().getArguments(), prop.getValues(), sel.getField().getName().startsWith("_"));
558            if (!vl.isEmpty())
559              processValues(context, sel, prop, target, vl, sel.getField().getName().startsWith("_"), inheritedList, suffix);
560          }
561        }
562      } else if (sel.getInlineFragment() != null) {
563        if (checkDirectives(sel.getInlineFragment().getDirectives())) {
564          if (Utilities.noString(sel.getInlineFragment().getTypeCondition()))
565            throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid?
566          if (source.fhirType().equals(sel.getInlineFragment().getTypeCondition())) 
567            processObject(context, source, target, sel.getInlineFragment().getSelectionSet(), inheritedList, suffix);
568        }
569      } else if (checkDirectives(sel.getFragmentSpread().getDirectives())) {
570        Fragment fragment = graphQL.getDocument().fragment(sel.getFragmentSpread().getName());
571        if (fragment == null)
572          throw new EGraphQLException("Unable to resolve fragment "+sel.getFragmentSpread().getName());
573
574        if (Utilities.noString(fragment.getTypeCondition()))
575          throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid?
576        if (source.fhirType().equals(fragment.getTypeCondition()))
577          processObject(context, source, target, fragment.getSelectionSet(), inheritedList, suffix);
578      }
579    }
580  }
581
582  private void processPrimitive(Argument arg, Base value) {
583    String s = value.fhirType();
584    if (s.equals("integer") || s.equals("decimal") || s.equals("unsignedInt") || s.equals("positiveInt"))
585      arg.addValue(new NumberValue(value.primitiveValue()));
586    else if (s.equals("boolean"))
587      arg.addValue(new NameValue(value.primitiveValue()));
588    else
589      arg.addValue(new StringValue(value.primitiveValue()));
590  }
591
592  private void processReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
593    if (!(source instanceof Reference))
594      throw new EGraphQLException("Not done yet");
595    if (services == null)
596      throw new EGraphQLException("Resource Referencing services not provided");
597
598    Reference ref = (Reference) source;
599    ReferenceResolution res = services.lookup(appInfo, context, ref);
600    if (res != null) {
601      if (targetTypeOk(field.getArguments(), res.getTarget())) {
602        Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList));
603        ObjectValue obj = new ObjectValue();
604        arg.addValue(obj);
605        processObject((Resource)res.getTargetContext(), (Base) res.getTarget(), obj, field.getSelectionSet(), inheritedList, suffix);
606      }
607    }
608    else if (!hasArgument(field.getArguments(), "optional", "true"))
609      throw new EGraphQLException("Unable to resolve reference to "+ref.getReference());
610  }
611
612  private void processCanonicalReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
613    if (!(source instanceof CanonicalType))
614      throw new EGraphQLException("Not done yet");
615    if (services == null)
616      throw new EGraphQLException("Resource Referencing services not provided");
617
618    Reference ref = new Reference(source.primitiveValue());
619    ReferenceResolution res = services.lookup(appInfo, context, ref);
620    if (res != null) {
621      if (targetTypeOk(field.getArguments(), res.getTarget())) {
622        Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList));
623        ObjectValue obj = new ObjectValue();
624        arg.addValue(obj);
625        processObject((Resource)res.getTargetContext(), (Base) res.getTarget(), obj, field.getSelectionSet(), inheritedList, suffix);
626      }
627    }
628    else if (!hasArgument(field.getArguments(), "optional", "true"))
629      throw new EGraphQLException("Unable to resolve reference to "+ref.getReference());
630  }
631
632  private ArgumentListStatus listStatus(Field field, boolean isList) {
633    if (field.hasDirective("singleton"))
634      return ArgumentListStatus.SINGLETON;
635    else if (isList)
636      return ArgumentListStatus.REPEATING;
637    else
638      return ArgumentListStatus.NOT_SPECIFIED;
639  }
640
641  private void processReverseReferenceList(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
642    if (services == null)
643      throw new EGraphQLException("Resource Referencing services not provided");
644    List<IBaseResource> list = new ArrayList<>();
645    List<Argument> params = new ArrayList<>();
646    Argument parg = null;
647    for (Argument a : field.getArguments())
648      if (!(a.getName().equals("_reference")))
649        params.add(a);
650      else if ((parg == null))
651        parg = a;
652      else
653        throw new EGraphQLException("Duplicate parameter _reference");
654    if (parg == null)
655      throw new EGraphQLException("Missing parameter _reference");
656    Argument arg = new Argument();
657    params.add(arg);
658    arg.setName(getSingleValue(parg));
659    arg.addValue(new StringValue(source.fhirType()+"/"+source.getId()));
660    services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), params, list);
661    arg = null;
662    ObjectValue obj = null;
663
664    List<Resource> vl = filterResources(field.argument("fhirpath"), list);
665    if (!vl.isEmpty()) {
666      arg = target.addField(field.getAlias()+suffix, listStatus(field, true));
667      for (Resource v : vl) {
668        obj = new ObjectValue();
669        arg.addValue(obj);
670        processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix);
671      }
672    }
673  }
674  
675  private void processReverseReferenceSearch(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
676    if (services == null)
677      throw new EGraphQLException("Resource Referencing services not provided");
678    List<Argument> params = new ArrayList<Argument>();
679    Argument parg = null;
680    for (Argument a : field.getArguments())
681      if (!(a.getName().equals("_reference")))
682        params.add(a);
683      else if ((parg == null))
684        parg = a;
685      else
686        throw new EGraphQLException("Duplicate parameter _reference");
687    if (parg == null)
688      throw new EGraphQLException("Missing parameter _reference");
689    Argument arg = new Argument();
690    params.add(arg);
691    arg.setName(getSingleValue(parg));
692    arg.addValue(new StringValue(source.fhirType()+"/"+source.getId()));
693    Bundle bnd = (Bundle) services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params);
694    Base bndWrapper = new SearchWrapper(field.getName(), bnd);
695    arg = target.addField(field.getAlias()+suffix, listStatus(field, false));
696    ObjectValue obj = new ObjectValue();
697    arg.addValue(obj);
698    processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix);
699  }
700
701  private void processSearch(ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
702    for (Selection sel : selection) {
703      if ((sel.getField() == null))
704        throw new EGraphQLException("Only field selections are allowed in this context");
705      checkNoDirectives(sel.getField().getDirectives());
706
707      if ((isResourceName(sel.getField().getName(), "")))
708        processSearchSingle(target, sel.getField(), inheritedList, suffix);
709      else if ((isResourceName(sel.getField().getName(), "List")))
710        processSearchSimple(target, sel.getField(), inheritedList, suffix);
711      else if ((isResourceName(sel.getField().getName(), "Connection")))
712        processSearchFull(target, sel.getField(), inheritedList, suffix);
713    }
714  }
715
716  private void processSearchSingle(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
717    if (services == null)
718      throw new EGraphQLException("Resource Referencing services not provided");
719    String id = "";
720    for (Argument arg : field.getArguments())
721      if ((arg.getName().equals("id")))
722        id = getSingleValue(arg);
723      else
724        throw new EGraphQLException("Unknown/invalid parameter "+arg.getName());
725    if (Utilities.noString(id))
726      throw new EGraphQLException("No id found");
727    Resource res = (Resource) services.lookup(appInfo, field.getName(), id);
728    if (res == null)
729      throw new EGraphQLException("Resource "+field.getName()+"/"+id+" not found");
730    Argument arg = target.addField(field.getAlias()+suffix, listStatus(field, false));
731    ObjectValue obj = new ObjectValue();
732    arg.addValue(obj);
733    processObject(res, res, obj, field.getSelectionSet(), inheritedList, suffix);
734  }
735
736  private void processSearchSimple(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
737    if (services == null)
738      throw new EGraphQLException("Resource Referencing services not provided");
739    List<IBaseResource> list = new ArrayList<>();
740    services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), field.getArguments(), list);
741    Argument arg = null;
742    ObjectValue obj = null;
743
744    List<Resource> vl = filterResources(field.argument("fhirpath"), list);
745    if (!vl.isEmpty()) {
746      arg = target.addField(field.getAlias()+suffix, listStatus(field, true));
747      for (Resource v : vl) {
748        obj = new ObjectValue();
749        arg.addValue(obj);
750        processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix);
751      }
752    }
753  }
754  
755  private void processSearchFull(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
756    if (services == null)
757      throw new EGraphQLException("Resource Referencing services not provided");
758    List<Argument> params = new ArrayList<Argument>();
759    Argument carg = null;
760    for ( Argument arg : field.getArguments())
761      if (arg.getName().equals("cursor"))
762        carg = arg;
763      else
764        params.add(arg);
765    if ((carg != null)) {
766      params.clear();;
767      String[] parts = getSingleValue(carg).split(":");
768      params.add(new Argument("search-id", new StringValue(parts[0])));
769      params.add(new Argument("search-offset", new StringValue(parts[1])));
770    }
771
772    Bundle bnd = (Bundle) services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params);
773    SearchWrapper bndWrapper = new SearchWrapper(field.getName(), bnd);
774    Argument arg = target.addField(field.getAlias()+suffix, listStatus(field, false));
775    ObjectValue obj = new ObjectValue();
776    arg.addValue(obj);
777    processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix);
778  }
779
780  private String getSingleValue(Argument arg) throws EGraphQLException {
781    List<Value> vl = resolveValues(arg, 1);
782    if (vl.size() == 0)
783      return "";
784    return vl.get(0).toString();
785  }
786
787  private List<Value> resolveValues(Argument arg) throws EGraphQLException {
788    return resolveValues(arg, -1, "");
789  }
790  
791  private List<Value> resolveValues(Argument arg, int max) throws EGraphQLException {
792    return resolveValues(arg, max, "");
793  }
794  
795  private List<Value> resolveValues(Argument arg, int max, String vars) throws EGraphQLException {
796    List<Value> result = new ArrayList<Value>();
797    for (Value v : arg.getValues()) {
798      if (! (v instanceof VariableValue))
799        result.add(v);
800      else {
801        if (vars.contains(":"+v.toString()+":"))
802          throw new EGraphQLException("Recursive reference to variable "+v.toString());
803        Argument a = workingVariables.get(v.toString());
804        if (a == null)
805          throw new EGraphQLException("No value found for variable \""+v.toString()+"\" in \""+arg.getName()+"\"");
806        List<Value> vl = resolveValues(a, -1, vars+":"+v.toString()+":");
807        result.addAll(vl);
808      }
809    }
810    if ((max != -1 && result.size() > max))
811      throw new EGraphQLException("Only "+Integer.toString(max)+" values are allowed for \""+arg.getName()+"\", but "+Integer.toString(result.size())+" enoucntered");
812    return result;
813  }
814
815
816  
817  
818  public Object getAppInfo() {
819    return appInfo;
820  }
821
822  @Override
823  public void setAppInfo(Object appInfo) {
824    this.appInfo = appInfo;
825  }
826
827  public Resource getFocus() {
828    return focus;
829  }
830
831  @Override
832  public void setFocus(IBaseResource focus) {
833    this.focus = (Resource) focus;
834  }
835
836  public Package getGraphQL() {
837    return graphQL;
838  }
839
840  @Override
841  public void setGraphQL(Package graphQL) {
842    this.graphQL = graphQL;
843  }
844
845  @Override
846  public GraphQLResponse getOutput() {
847    return output;
848  }
849
850  public IGraphQLStorageServices getServices() {
851    return services;
852  }
853
854  @Override
855  public void setServices(IGraphQLStorageServices services) {
856    this.services = services;
857  }
858
859
860  //
861//{ GraphQLSearchWrapper }
862//
863//constructor GraphQLSearchWrapper.Create(bundle : Bundle);
864//var
865//  s : String;
866//{
867//  inherited Create;
868//  FBundle = bundle;
869//  s = bundle_List.Matches["self"];
870//  FParseMap = TParseMap.create(s.Substring(s.IndexOf("?")+1));
871//}
872//
873//destructor GraphQLSearchWrapper.Destroy;
874//{
875//  FParseMap.free;
876//  FBundle.Free;
877//  inherited;
878//}
879//
880//function GraphQLSearchWrapper.extractLink(name: String): String;
881//var
882//  s : String;
883//  pm : TParseMap;
884//{
885//  s = FBundle_List.Matches[name];
886//  if (s == "")
887//    result = null
888//  else
889//  {
890//    pm = TParseMap.create(s.Substring(s.IndexOf("?")+1));
891//    try
892//      result = String.Create(pm.GetVar("search-id")+":"+pm.GetVar("search-offset"));
893//    finally
894//      pm.Free;
895//    }
896//  }
897//}
898//
899//function GraphQLSearchWrapper.extractParam(name: String; int : boolean): Base;
900//var
901//  s : String;
902//{
903//  s = FParseMap.GetVar(name);
904//  if (s == "")
905//    result = null
906//  else if (int)
907//    result = Integer.Create(s)
908//  else
909//    result = String.Create(s);
910//}
911//
912//function GraphQLSearchWrapper.fhirType(): String;
913//{
914//  result = "*Connection";
915//}
916//
917//  // 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
918//
919//function GraphQLSearchWrapper.getPropertyValue(propName: string): Property;
920//var
921//  list : List<GraphQLSearchEdge>;
922//  be : BundleEntry;
923//{
924//  if (propName == "first")
925//    result = Property.Create(self, propname, "string", false, String, extractLink("first"))
926//  else if (propName == "previous")
927//    result = Property.Create(self, propname, "string", false, String, extractLink("previous"))
928//  else if (propName == "next")
929//    result = Property.Create(self, propname, "string", false, String, extractLink("next"))
930//  else if (propName == "last")
931//    result = Property.Create(self, propname, "string", false, String, extractLink("last"))
932//  else if (propName == "count")
933//    result = Property.Create(self, propname, "integer", false, String, FBundle.totalElement)
934//  else if (propName == "offset")
935//    result = Property.Create(self, propname, "integer", false, Integer, extractParam("search-offset", true))
936//  else if (propName == "pagesize")
937//    result = Property.Create(self, propname, "integer", false, Integer, extractParam("_count", true))
938//  else if (propName == "edges")
939//  {
940//    list = ArrayList<GraphQLSearchEdge>();
941//    try
942//      for be in FBundle.getEntry() do
943//        list.add(GraphQLSearchEdge.create(be));
944//      result = Property.Create(self, propname, "integer", true, Integer, List<Base>(list));
945//    finally
946//      list.Free;
947//    }
948//  }
949//  else
950//    result = null;
951//}
952//
953//private void GraphQLSearchWrapper.SetBundle(const Value: Bundle);
954//{
955//  FBundle.Free;
956//  FBundle = Value;
957//}
958//
959//{ GraphQLSearchEdge }
960//
961//constructor GraphQLSearchEdge.Create(entry: BundleEntry);
962//{
963//  inherited Create;
964//  FEntry = entry;
965//}
966//
967//destructor GraphQLSearchEdge.Destroy;
968//{
969//  FEntry.Free;
970//  inherited;
971//}
972//
973//function GraphQLSearchEdge.fhirType(): String;
974//{
975//  result = "*Edge";
976//}
977//
978//function GraphQLSearchEdge.getPropertyValue(propName: string): Property;
979//{
980//  if (propName == "mode")
981//  {
982//    if (FEntry.search != null)
983//      result = Property.Create(self, propname, "code", false, Enum, FEntry.search.modeElement)
984//    else
985//      result = Property.Create(self, propname, "code", false, Enum, Base(null));
986//  }
987//  else if (propName == "score")
988//  {
989//    if (FEntry.search != null)
990//      result = Property.Create(self, propname, "decimal", false, Decimal, FEntry.search.scoreElement)
991//    else
992//      result = Property.Create(self, propname, "decimal", false, Decimal, Base(null));
993//  }
994//  else if (propName == "resource")
995//    result = Property.Create(self, propname, "resource", false, Resource, FEntry.getResource())
996//  else
997//    result = null;
998//}
999//
1000//private void GraphQLSearchEdge.SetEntry(const Value: BundleEntry);
1001//{
1002//  FEntry.Free;
1003//  FEntry = value;
1004//}
1005//
1006}