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