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