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