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