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