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