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