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