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