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