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