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, String fieldName, 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
219      // Account for situations where the GraphQL expression selected e.g.
220      // effectiveDateTime but the field contains effectivePeriod
221      String propName = prop.getName();
222      List<Base> newValues = new ArrayList<>(values.size());
223      for (Base value : values) {
224        if (propName.endsWith("[x]")) {
225          String propNameShortened = propName.substring(0, propName.length() - 3);
226          if (fieldName.startsWith(propNameShortened) && fieldName.length() > propNameShortened.length()) {
227            if (!value.fhirType().equalsIgnoreCase(fieldName.substring(propNameShortened.length()))) {
228              continue;
229            }
230          }
231        }
232        newValues.add(value);
233      }
234
235      int i = 0;
236      int t = 0;
237      if (fp.length() == 0)
238        for (Base v : newValues) {
239
240          if ((i >= offset) && passesExtensionMode(v, extensionMode)) {
241            result.add(v);
242            t++;
243            if (t >= count)
244              break;
245          }
246          i++;
247        }
248      else {
249        ExpressionNode node = fpe.parse(fp.substring(5));
250        for (Base v : newValues) {
251          if ((i >= offset) && passesExtensionMode(v, extensionMode) && fpe.evaluateToBoolean(null, context, v, node)) {
252            result.add(v);
253            t++;
254            if (t >= count)
255              break;
256          }
257          i++;
258        }
259      }
260    }
261    return result;
262  }
263
264  private List<Resource> filterResources(Argument fhirpath, Bundle bnd) throws EGraphQLException, FHIRException {
265    List<Resource> result = new ArrayList<Resource>();
266    if (bnd.getEntry().size() > 0) {
267      if ((fhirpath == null))
268        for (BundleEntryComponent be : bnd.getEntry())
269          result.add(be.getResource());
270      else {
271        FHIRPathEngine fpe = new FHIRPathEngine(context);
272        ExpressionNode node = fpe.parse(getSingleValue(fhirpath));
273        for (BundleEntryComponent be : bnd.getEntry())
274          if (fpe.evaluateToBoolean(null, be.getResource(), be.getResource(), node))
275            result.add(be.getResource());
276      }
277    }
278    return result;
279  }
280
281  private List<Resource> filterResources(Argument fhirpath, List<IBaseResource> list) throws EGraphQLException, FHIRException {
282    List<Resource> result = new ArrayList<Resource>();
283    if (list.size() > 0) {
284      if ((fhirpath == null))
285        for (IBaseResource v : list)
286          result.add((Resource) v);
287      else {
288        FHIRPathEngine fpe = new FHIRPathEngine(context);
289        ExpressionNode node = fpe.parse(getSingleValue(fhirpath));
290        for (IBaseResource v : list)
291          if (fpe.evaluateToBoolean(null, (Resource) v, (Base) v, node))
292            result.add((Resource) v);
293      }
294    }
295    return result;
296  }
297
298  private boolean hasArgument(List<Argument> arguments, String name, String value) {
299    for (Argument arg : arguments)
300      if ((arg.getName().equals(name)) && arg.hasValue(value))
301        return true;
302    return false;
303  }
304
305  private void processValues(Resource context, Selection sel, Property prop, ObjectValue target, List<Base> values, boolean extensionMode, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
306    boolean il = false;
307    Argument arg = null;
308    ExpressionNode expression = null;
309    if (sel.getField().hasDirective("slice")) {
310      Directive dir = sel.getField().directive("slice");
311      String s = ((StringValue) dir.getArguments().get(0).getValues().get(0)).getValue();
312      if (s.equals("$index"))
313        expression = magicExpression;
314      else
315        expression = fpe.parse(s);
316    }
317    if (sel.getField().hasDirective("flatten")) // special: instruction to drop this node...
318      il = prop.isList() && !sel.getField().hasDirective("first");
319    else if (sel.getField().hasDirective("first")) {
320      if (expression != null)
321        throw new FHIRException("You cannot mix @slice and @first");
322      arg = target.addField(sel.getField().getAlias() + suffix, listStatus(sel.getField(), inheritedList));
323    } else if (expression == null)
324      arg = target.addField(sel.getField().getAlias() + suffix, listStatus(sel.getField(), prop.isList() || inheritedList));
325
326
327    int index = 0;
328    for (Base value : values) {
329      String ss = "";
330      if (expression != null) {
331        if (expression == magicExpression)
332          ss = suffix + '.' + Integer.toString(index);
333        else
334          ss = suffix+'.'+fpe.evaluateToString(null, null, null, value, expression);
335        if (!sel.getField().hasDirective("flatten"))
336          arg = target.addField(sel.getField().getAlias() + suffix, listStatus(sel.getField(), prop.isList() || inheritedList));
337      }
338
339      if (value.isPrimitive() && !extensionMode) {
340        if (!sel.getField().getSelectionSet().isEmpty())
341          throw new EGraphQLException("Encountered a selection set on a scalar field type");
342        processPrimitive(arg, value);
343      } else {
344        if (sel.getField().getSelectionSet().isEmpty())
345          throw new EGraphQLException("No Fields selected on a complex object");
346        if (arg == null)
347          processObject(context, value, target, sel.getField().getSelectionSet(), il, ss);
348        else {
349          ObjectValue n = new ObjectValue();
350          arg.addValue(n);
351          processObject(context, value, n, sel.getField().getSelectionSet(), il, ss);
352        }
353      }
354      if (sel.getField().hasDirective("first"))
355        return;
356      index++;
357    }
358  }
359
360  private void processVariables(Operation op) throws EGraphQLException {
361    for (Variable varRef : op.getVariables()) {
362      Argument varDef = null;
363      for (Argument v : graphQL.getVariables())
364        if (v.getName().equals(varRef.getName()))
365          varDef = v;
366      if (varDef != null)
367        workingVariables.put(varRef.getName(), varDef); // todo: check type?
368      else if (varRef.getDefaultValue() != null)
369        workingVariables.put(varRef.getName(), new Argument(varRef.getName(), varRef.getDefaultValue()));
370      else
371        throw new EGraphQLException("No value found for variable ");
372    }
373  }
374
375  private boolean isPrimitive(String typename) {
376    return Utilities.existsInList(typename, "boolean", "integer", "string", "decimal", "uri", "base64Binary", "instant", "date", "dateTime", "time", "code", "oid", "id", "markdown", "unsignedInt", "positiveInt", "url", "canonical");
377  }
378
379  private boolean isResourceName(String name, String suffix) {
380    if (!name.endsWith(suffix))
381      return false;
382    name = name.substring(0, name.length() - suffix.length());
383    return context.getResourceNamesAsSet().contains(name);
384  }
385
386  private void processObject(Resource context, Base source, ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
387    for (Selection sel : selection) {
388      if (sel.getField() != null) {
389        if (checkDirectives(sel.getField().getDirectives())) {
390          Property prop = source.getNamedProperty(sel.getField().getName());
391          if ((prop == null) && sel.getField().getName().startsWith("_"))
392            prop = source.getNamedProperty(sel.getField().getName().substring(1));
393          if (prop == null) {
394            if ((sel.getField().getName().equals("resourceType") && source instanceof Resource))
395              target.addField("resourceType", listStatus(sel.getField(), false)).addValue(new StringValue(source.fhirType()));
396            else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("Reference")))
397              processReference(context, source, sel.getField(), target, inheritedList, suffix);
398            else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("canonical")))
399              processCanonicalReference(context, source, sel.getField(), target, inheritedList, suffix);
400            else if (isResourceName(sel.getField().getName(), "List") && (source instanceof Resource))
401              processReverseReferenceList((Resource) source, sel.getField(), target, inheritedList, suffix);
402            else if (isResourceName(sel.getField().getName(), "Connection") && (source instanceof Resource))
403              processReverseReferenceSearch((Resource) source, sel.getField(), target, inheritedList, suffix);
404            else
405              throw new EGraphQLException("Unknown property " + sel.getField().getName() + " on " + source.fhirType());
406          } else {
407            if (!isPrimitive(prop.getTypeCode()) && sel.getField().getName().startsWith("_"))
408              throw new EGraphQLException("Unknown property " + sel.getField().getName() + " on " + source.fhirType());
409
410            List<Base> vl = filter(context, prop, sel.getField().getName(), sel.getField().getArguments(), prop.getValues(), sel.getField().getName().startsWith("_"));
411            if (!vl.isEmpty())
412              processValues(context, sel, prop, target, vl, sel.getField().getName().startsWith("_"), inheritedList, suffix);
413          }
414        }
415      } else if (sel.getInlineFragment() != null) {
416        if (checkDirectives(sel.getInlineFragment().getDirectives())) {
417          if (Utilities.noString(sel.getInlineFragment().getTypeCondition()))
418            throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid?
419          if (source.fhirType().equals(sel.getInlineFragment().getTypeCondition()))
420            processObject(context, source, target, sel.getInlineFragment().getSelectionSet(), inheritedList, suffix);
421        }
422      } else if (checkDirectives(sel.getFragmentSpread().getDirectives())) {
423        Fragment fragment = graphQL.getDocument().fragment(sel.getFragmentSpread().getName());
424        if (fragment == null)
425          throw new EGraphQLException("Unable to resolve fragment " + sel.getFragmentSpread().getName());
426
427        if (Utilities.noString(fragment.getTypeCondition()))
428          throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid?
429        if (source.fhirType().equals(fragment.getTypeCondition()))
430          processObject(context, source, target, fragment.getSelectionSet(), inheritedList, suffix);
431      }
432    }
433  }
434
435  private void processPrimitive(Argument arg, Base value) {
436    String s = value.fhirType();
437    if (s.equals("integer") || s.equals("decimal") || s.equals("unsignedInt") || s.equals("positiveInt"))
438      arg.addValue(new NumberValue(value.primitiveValue()));
439    else if (s.equals("boolean"))
440      arg.addValue(new NameValue(value.primitiveValue()));
441    else
442      arg.addValue(new StringValue(value.primitiveValue()));
443  }
444
445  private void processReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
446    if (!(source instanceof Reference))
447      throw new EGraphQLException("Not done yet");
448    if (services == null)
449      throw new EGraphQLException("Resource Referencing services not provided");
450
451    Reference ref = (Reference) source;
452    ReferenceResolution res = services.lookup(appInfo, context, ref);
453    if (res != null) {
454      if (targetTypeOk(field.getArguments(), res.getTarget())) {
455        Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList));
456        ObjectValue obj = new ObjectValue();
457        arg.addValue(obj);
458        processObject((Resource) res.getTargetContext(), (Base) res.getTarget(), obj, field.getSelectionSet(), inheritedList, suffix);
459      }
460    } else if (!hasArgument(field.getArguments(), "optional", "true"))
461      throw new EGraphQLException("Unable to resolve reference to " + ref.getReference());
462  }
463
464  private void processCanonicalReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
465    if (!(source instanceof CanonicalType))
466      throw new EGraphQLException("Not done yet");
467    if (services == null)
468      throw new EGraphQLException("Resource Referencing services not provided");
469
470    Reference ref = new Reference(source.primitiveValue());
471    ReferenceResolution res = services.lookup(appInfo, context, ref);
472    if (res != null) {
473      if (targetTypeOk(field.getArguments(), res.getTarget())) {
474        Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList));
475        ObjectValue obj = new ObjectValue();
476        arg.addValue(obj);
477        processObject((Resource) res.getTargetContext(), (Base) res.getTarget(), obj, field.getSelectionSet(), inheritedList, suffix);
478      }
479    } else if (!hasArgument(field.getArguments(), "optional", "true"))
480      throw new EGraphQLException("Unable to resolve reference to " + ref.getReference());
481  }
482
483  private ArgumentListStatus listStatus(Field field, boolean isList) {
484    if (field.hasDirective("singleton"))
485      return ArgumentListStatus.SINGLETON;
486    else if (isList)
487      return ArgumentListStatus.REPEATING;
488    else
489      return ArgumentListStatus.NOT_SPECIFIED;
490  }
491
492  private void processReverseReferenceList(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
493    if (services == null)
494      throw new EGraphQLException("Resource Referencing services not provided");
495    List<IBaseResource> list = new ArrayList<>();
496    List<Argument> params = new ArrayList<Argument>();
497    Argument parg = null;
498    for (Argument a : field.getArguments())
499      if (!(a.getName().equals("_reference")))
500        params.add(a);
501      else if ((parg == null))
502        parg = a;
503      else
504        throw new EGraphQLException("Duplicate parameter _reference");
505    if (parg == null)
506      throw new EGraphQLException("Missing parameter _reference");
507    Argument arg = new Argument();
508    params.add(arg);
509    arg.setName(getSingleValue(parg));
510    arg.addValue(new StringValue(source.fhirType() + "/" + source.getId()));
511    services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), params, list);
512    arg = null;
513    ObjectValue obj = null;
514
515    List<Resource> vl = filterResources(field.argument("fhirpath"), list);
516    if (!vl.isEmpty()) {
517      arg = target.addField(field.getAlias() + suffix, listStatus(field, true));
518      for (Resource v : vl) {
519        obj = new ObjectValue();
520        arg.addValue(obj);
521        processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix);
522      }
523    }
524  }
525
526  private void processReverseReferenceSearch(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
527    if (services == null)
528      throw new EGraphQLException("Resource Referencing services not provided");
529    List<Argument> params = new ArrayList<Argument>();
530    Argument parg = null;
531    for (Argument a : field.getArguments())
532      if (!(a.getName().equals("_reference")))
533        params.add(a);
534      else if ((parg == null))
535        parg = a;
536      else
537        throw new EGraphQLException("Duplicate parameter _reference");
538    if (parg == null)
539      throw new EGraphQLException("Missing parameter _reference");
540    Argument arg = new Argument();
541    params.add(arg);
542    arg.setName(getSingleValue(parg));
543    arg.addValue(new StringValue(source.fhirType() + "/" + source.getId()));
544    Bundle bnd = (Bundle) services.search(appInfo, field.getName().substring(0, field.getName().length() - 10), params);
545    Base bndWrapper = new SearchWrapper(field.getName(), bnd);
546    arg = target.addField(field.getAlias() + suffix, listStatus(field, false));
547    ObjectValue obj = new ObjectValue();
548    arg.addValue(obj);
549    processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix);
550  }
551
552  private void processSearch(ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
553    for (Selection sel : selection) {
554      if ((sel.getField() == null))
555        throw new EGraphQLException("Only field selections are allowed in this context");
556      checkNoDirectives(sel.getField().getDirectives());
557
558      if ((isResourceName(sel.getField().getName(), "")))
559        processSearchSingle(target, sel.getField(), inheritedList, suffix);
560      else if ((isResourceName(sel.getField().getName(), "List")))
561        processSearchSimple(target, sel.getField(), inheritedList, suffix);
562      else if ((isResourceName(sel.getField().getName(), "Connection")))
563        processSearchFull(target, sel.getField(), inheritedList, suffix);
564    }
565  }
566
567  private void processSearchSingle(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
568    if (services == null)
569      throw new EGraphQLException("Resource Referencing services not provided");
570    String id = "";
571    for (Argument arg : field.getArguments())
572      if ((arg.getName().equals("id")))
573        id = getSingleValue(arg);
574      else
575        throw new EGraphQLException("Unknown/invalid parameter " + arg.getName());
576    if (Utilities.noString(id))
577      throw new EGraphQLException("No id found");
578    Resource res = (Resource) services.lookup(appInfo, field.getName(), id);
579    if (res == null)
580      throw new EGraphQLException("Resource " + field.getName() + "/" + id + " not found");
581    Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, false));
582    ObjectValue obj = new ObjectValue();
583    arg.addValue(obj);
584    processObject(res, res, obj, field.getSelectionSet(), inheritedList, suffix);
585  }
586
587  private void processSearchSimple(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
588    if (services == null)
589      throw new EGraphQLException("Resource Referencing services not provided");
590    List<IBaseResource> list = new ArrayList<>();
591    services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), field.getArguments(), list);
592    Argument arg = null;
593    ObjectValue obj = null;
594
595    List<Resource> vl = filterResources(field.argument("fhirpath"), list);
596    if (!vl.isEmpty()) {
597      arg = target.addField(field.getAlias() + suffix, listStatus(field, true));
598      for (Resource v : vl) {
599        obj = new ObjectValue();
600        arg.addValue(obj);
601        processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix);
602      }
603    }
604  }
605
606  private void processSearchFull(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException {
607    if (services == null)
608      throw new EGraphQLException("Resource Referencing services not provided");
609    List<Argument> params = new ArrayList<Argument>();
610    Argument carg = null;
611    for (Argument arg : field.getArguments())
612      if (arg.getName().equals("cursor"))
613        carg = arg;
614      else
615        params.add(arg);
616    if ((carg != null)) {
617      params.clear();
618      ;
619      String[] parts = getSingleValue(carg).split(":");
620      params.add(new Argument("search-id", new StringValue(parts[0])));
621      params.add(new Argument("search-offset", new StringValue(parts[1])));
622    }
623
624    Bundle bnd = (Bundle) services.search(appInfo, field.getName().substring(0, field.getName().length() - 10), params);
625    SearchWrapper bndWrapper = new SearchWrapper(field.getName(), bnd);
626    Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, false));
627    ObjectValue obj = new ObjectValue();
628    arg.addValue(obj);
629    processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix);
630  }
631
632  private String getSingleValue(Argument arg) throws EGraphQLException {
633    List<Value> vl = resolveValues(arg, 1);
634    if (vl.size() == 0)
635      return "";
636    return vl.get(0).toString();
637  }
638
639  private List<Value> resolveValues(Argument arg) throws EGraphQLException {
640    return resolveValues(arg, -1, "");
641  }
642
643  private List<Value> resolveValues(Argument arg, int max) throws EGraphQLException {
644    return resolveValues(arg, max, "");
645  }
646
647  private List<Value> resolveValues(Argument arg, int max, String vars) throws EGraphQLException {
648    List<Value> result = new ArrayList<Value>();
649    for (Value v : arg.getValues()) {
650      if (!(v instanceof VariableValue))
651        result.add(v);
652      else {
653        if (vars.contains(":" + v.toString() + ":"))
654          throw new EGraphQLException("Recursive reference to variable " + v.toString());
655        Argument a = workingVariables.get(v.toString());
656        if (a == null)
657          throw new EGraphQLException("No value found for variable \"" + v.toString() + "\" in \"" + arg.getName() + "\"");
658        List<Value> vl = resolveValues(a, -1, vars + ":" + v.toString() + ":");
659        result.addAll(vl);
660      }
661    }
662    if ((max != -1 && result.size() > max))
663      throw new EGraphQLException("Only " + Integer.toString(max) + " values are allowed for \"" + arg.getName() + "\", but " + Integer.toString(result.size()) + " enoucntered");
664    return result;
665  }
666
667  public Object getAppInfo() {
668    return appInfo;
669  }
670
671  public void setAppInfo(Object appInfo) {
672    this.appInfo = appInfo;
673  }
674
675  public Resource getFocus() {
676    return focus;
677  }
678
679  @Override
680  public void setFocus(IBaseResource focus) {
681    this.focus = (Resource) focus;
682  }
683
684  public Package getGraphQL() {
685    return graphQL;
686  }
687
688  @Override
689  public void setGraphQL(Package graphQL) {
690    this.graphQL = graphQL;
691  }
692
693  public GraphQLResponse getOutput() {
694    return output;
695  }
696
697  public IGraphQLStorageServices getServices() {
698    return services;
699  }
700
701  @Override
702  public void setServices(IGraphQLStorageServices services) {
703    this.services = services;
704  }
705
706  public static class SearchEdge extends Base {
707
708    private BundleEntryComponent be;
709    private String type;
710
711    SearchEdge(String type, BundleEntryComponent be) {
712      this.type = type;
713      this.be = be;
714    }
715
716    @Override
717    public String fhirType() {
718      return type;
719    }
720
721    @Override
722    protected void listChildren(List<Property> result) {
723      throw new Error("Not Implemented");
724    }
725
726    @Override
727    public String getIdBase() {
728      throw new Error("Not Implemented");
729    }
730
731    @Override
732    public void setIdBase(String value) {
733      throw new Error("Not Implemented");
734    }
735
736    @Override
737    public Property getNamedProperty(int _hash, String _name, boolean _checkValid) throws FHIRException {
738      switch (_hash) {
739        case 3357091:    /*mode*/
740          return new Property(_name, "string", "n/a", 0, 1, be.getSearch().hasMode() ? be.getSearch().getModeElement() : null);
741        case 109264530:  /*score*/
742          return new Property(_name, "string", "n/a", 0, 1, be.getSearch().hasScore() ? be.getSearch().getScoreElement() : null);
743        case -341064690: /*resource*/
744          return new Property(_name, "resource", "n/a", 0, 1, be.hasResource() ? be.getResource() : null);
745        default:
746          return super.getNamedProperty(_hash, _name, _checkValid);
747      }
748    }
749  }
750
751  public static class SearchWrapper extends Base {
752
753    private Bundle bnd;
754    private String type;
755    private Map<String, String> map;
756
757    SearchWrapper(String type, Bundle bnd) throws FHIRException {
758      this.type = type;
759      this.bnd = bnd;
760      for (BundleLinkComponent bl : bnd.getLink())
761        if (bl.getRelation().equals("self"))
762          map = parseURL(bl.getUrl());
763    }
764
765    @Override
766    public String fhirType() {
767      return type;
768    }
769
770    @Override
771    protected void listChildren(List<Property> result) {
772      throw new Error("Not Implemented");
773    }
774
775    @Override
776    public String getIdBase() {
777      throw new Error("Not Implemented");
778    }
779
780    @Override
781    public void setIdBase(String value) {
782      throw new Error("Not Implemented");
783    }
784
785    @Override
786    public Property getNamedProperty(int _hash, String _name, boolean _checkValid) throws FHIRException {
787      switch (_hash) {
788        case 97440432:   /*first*/
789          return new Property(_name, "string", "n/a", 0, 1, extractLink(_name));
790        case -1273775369: /*previous*/
791          return new Property(_name, "string", "n/a", 0, 1, extractLink(_name));
792        case 3377907:    /*next*/
793          return new Property(_name, "string", "n/a", 0, 1, extractLink(_name));
794        case 3314326:    /*last*/
795          return new Property(_name, "string", "n/a", 0, 1, extractLink(_name));
796        case 94851343:   /*count*/
797          return new Property(_name, "integer", "n/a", 0, 1, bnd.getTotalElement());
798        case -1019779949:/*offset*/
799          return new Property(_name, "integer", "n/a", 0, 1, extractParam("search-offset"));
800        case 860381968:  /*pagesize*/
801          return new Property(_name, "integer", "n/a", 0, 1, extractParam("_count"));
802        case 96356950:  /*edges*/
803          return new Property(_name, "edge", "n/a", 0, Integer.MAX_VALUE, getEdges());
804        default:
805          return super.getNamedProperty(_hash, _name, _checkValid);
806      }
807    }
808
809    private List<Base> getEdges() {
810      List<Base> list = new ArrayList<>();
811      for (BundleEntryComponent be : bnd.getEntry())
812        list.add(new SearchEdge(type.substring(0, type.length() - 10) + "Edge", be));
813      return list;
814    }
815
816    private Base extractParam(String name) throws FHIRException {
817      return map != null ? new IntegerType(map.get(name)) : null;
818    }
819
820    private Map<String, String> parseURL(String url) throws FHIRException {
821      try {
822        Map<String, String> map = new HashMap<String, String>();
823        String[] pairs = url.split("&");
824        for (String pair : pairs) {
825          int idx = pair.indexOf("=");
826          String key;
827          key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair;
828          String value = idx > 0 && pair.length() > idx + 1 ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null;
829          map.put(key, value);
830        }
831        return map;
832      } catch (UnsupportedEncodingException e) {
833        throw new FHIRException(e);
834      }
835    }
836
837    private Base extractLink(String _name) throws FHIRException {
838      for (BundleLinkComponent bl : bnd.getLink()) {
839        if (bl.getRelation().equals(_name)) {
840          Map<String, String> map = parseURL(bl.getUrl());
841          return new StringType(map.get("search-id") + ':' + map.get("search-offset"));
842        }
843      }
844      return null;
845    }
846
847  }
848
849
850  //
851//{ GraphQLSearchWrapper }
852//
853//constructor GraphQLSearchWrapper.Create(bundle : Bundle);
854//var
855//  s : String;
856//{
857//  inherited Create;
858//  FBundle = bundle;
859//  s = bundle_List.Matches["self"];
860//  FParseMap = TParseMap.create(s.Substring(s.IndexOf("?")+1));
861//}
862//
863//destructor GraphQLSearchWrapper.Destroy;
864//{
865//  FParseMap.free;
866//  FBundle.Free;
867//  inherited;
868//}
869//
870//function GraphQLSearchWrapper.extractLink(name: String): String;
871//var
872//  s : String;
873//  pm : TParseMap;
874//{
875//  s = FBundle_List.Matches[name];
876//  if (s == "")
877//    result = null
878//  else
879//  {
880//    pm = TParseMap.create(s.Substring(s.IndexOf("?")+1));
881//    try
882//      result = String.Create(pm.GetVar("search-id")+":"+pm.GetVar("search-offset"));
883//    finally
884//      pm.Free;
885//    }
886//  }
887//}
888//
889//function GraphQLSearchWrapper.extractParam(name: String; int : boolean): Base;
890//var
891//  s : String;
892//{
893//  s = FParseMap.GetVar(name);
894//  if (s == "")
895//    result = null
896//  else if (int)
897//    result = Integer.Create(s)
898//  else
899//    result = String.Create(s);
900//}
901//
902//function GraphQLSearchWrapper.fhirType(): String;
903//{
904//  result = "*Connection";
905//}
906//
907//  // 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
908//
909//function GraphQLSearchWrapper.getPropertyValue(propName: string): Property;
910//var
911//  list : List<GraphQLSearchEdge>;
912//  be : BundleEntry;
913//{
914//  if (propName == "first")
915//    result = Property.Create(self, propname, "string", false, String, extractLink("first"))
916//  else if (propName == "previous")
917//    result = Property.Create(self, propname, "string", false, String, extractLink("previous"))
918//  else if (propName == "next")
919//    result = Property.Create(self, propname, "string", false, String, extractLink("next"))
920//  else if (propName == "last")
921//    result = Property.Create(self, propname, "string", false, String, extractLink("last"))
922//  else if (propName == "count")
923//    result = Property.Create(self, propname, "integer", false, String, FBundle.totalElement)
924//  else if (propName == "offset")
925//    result = Property.Create(self, propname, "integer", false, Integer, extractParam("search-offset", true))
926//  else if (propName == "pagesize")
927//    result = Property.Create(self, propname, "integer", false, Integer, extractParam("_count", true))
928//  else if (propName == "edges")
929//  {
930//    list = ArrayList<GraphQLSearchEdge>();
931//    try
932//      for be in FBundle.getEntry() do
933//        list.add(GraphQLSearchEdge.create(be));
934//      result = Property.Create(self, propname, "integer", true, Integer, List<Base>(list));
935//    finally
936//      list.Free;
937//    }
938//  }
939//  else
940//    result = null;
941//}
942//
943//private void GraphQLSearchWrapper.SetBundle(const Value: Bundle);
944//{
945//  FBundle.Free;
946//  FBundle = Value;
947//}
948//
949//{ GraphQLSearchEdge }
950//
951//constructor GraphQLSearchEdge.Create(entry: BundleEntry);
952//{
953//  inherited Create;
954//  FEntry = entry;
955//}
956//
957//destructor GraphQLSearchEdge.Destroy;
958//{
959//  FEntry.Free;
960//  inherited;
961//}
962//
963//function GraphQLSearchEdge.fhirType(): String;
964//{
965//  result = "*Edge";
966//}
967//
968//function GraphQLSearchEdge.getPropertyValue(propName: string): Property;
969//{
970//  if (propName == "mode")
971//  {
972//    if (FEntry.search != null)
973//      result = Property.Create(self, propname, "code", false, Enum, FEntry.search.modeElement)
974//    else
975//      result = Property.Create(self, propname, "code", false, Enum, Base(null));
976//  }
977//  else if (propName == "score")
978//  {
979//    if (FEntry.search != null)
980//      result = Property.Create(self, propname, "decimal", false, Decimal, FEntry.search.scoreElement)
981//    else
982//      result = Property.Create(self, propname, "decimal", false, Decimal, Base(null));
983//  }
984//  else if (propName == "resource")
985//    result = Property.Create(self, propname, "resource", false, Resource, FEntry.getResource())
986//  else
987//    result = null;
988//}
989//
990//private void GraphQLSearchEdge.SetEntry(const Value: BundleEntry);
991//{
992//  FEntry.Free;
993//  FEntry = value;
994//}
995//
996}