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