001package org.hl7.fhir.r5.utils;
002
003import java.io.UnsupportedEncodingException;
004import java.net.URLDecoder;
005import java.util.ArrayList;
006import java.util.LinkedHashMap;
007import java.util.LinkedList;
008import java.util.List;
009import java.util.Map;
010
011import org.apache.xmlbeans.xml.stream.ReferenceResolver;
012import org.hl7.fhir.exceptions.FHIRException;
013import org.hl7.fhir.r5.context.IWorkerContext;
014import org.hl7.fhir.r5.model.Base;
015import org.hl7.fhir.r5.model.Bundle;
016import org.hl7.fhir.r5.model.Expression;
017import org.hl7.fhir.r5.model.ExpressionNode;
018import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent;
019import org.hl7.fhir.r5.model.GraphDefinition;
020import org.hl7.fhir.r5.model.GraphDefinition.GraphDefinitionLinkComponent;
021import org.hl7.fhir.r5.model.Reference;
022import org.hl7.fhir.r5.model.Resource;
023import org.hl7.fhir.r5.model.StringType;
024import org.hl7.fhir.utilities.Utilities;
025import org.hl7.fhir.utilities.graphql.Argument;
026import org.hl7.fhir.utilities.graphql.EGraphEngine;
027import org.hl7.fhir.utilities.graphql.EGraphQLException;
028import org.hl7.fhir.utilities.graphql.GraphQLResponse;
029import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
030import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices.ReferenceResolution;
031import org.hl7.fhir.utilities.graphql.StringValue;
032import org.hl7.fhir.instance.model.api.IBaseResource;
033
034public class GraphDefinitionEngine {
035
036
037  private static final String TAG_NAME = "Compiled.expression";
038  
039  private IGraphQLStorageServices services;
040  private IWorkerContext context;
041  /**
042   *  for the host to pass context into and get back on the reference resolution interface
043   */
044  private Object appInfo;
045
046  /**
047   *  the focus resource - if (there instanceof one. if (there isn"t,) there instanceof no focus
048   */
049  private Resource start;
050
051  /**
052   * The package that describes the graphQL to be executed, operation name, and variables
053   */
054  private GraphDefinition graphDefinition;
055
056  /**
057   * If the graph definition is being run to validate a grph
058   */
059  private boolean validating;
060  
061  /**
062   * where the output from executing the query instanceof going to go
063   */
064  private Bundle bundle;
065
066  private String baseURL;
067  private FHIRPathEngine engine;
068
069  public GraphDefinitionEngine(IGraphQLStorageServices services, IWorkerContext context) {
070    super();
071    this.services = services;
072    this.context = context;
073  }
074
075  public Object getAppInfo() {
076    return appInfo;
077  }
078
079  public void setAppInfo(Object appInfo) {
080    this.appInfo = appInfo;
081  }
082
083  public Resource getFocus() {
084    return start;
085  }
086
087  public void setFocus(Resource focus) {
088    this.start = focus;
089  }
090
091  public GraphDefinition getGraphDefinition() {
092    return graphDefinition;
093  }
094
095  public void setGraphDefinition(GraphDefinition graphDefinition) {
096    this.graphDefinition = graphDefinition;
097  }
098
099  public Bundle getOutput() {
100    return bundle;
101  }
102
103  public void setOutput(Bundle bundle) {
104    this.bundle = bundle;
105  }
106
107  public IGraphQLStorageServices getServices() {
108    return services;
109  }
110
111  public IWorkerContext getContext() {
112    return context;
113  }
114
115  public String getBaseURL() {
116    return baseURL;
117  }
118
119  public void setBaseURL(String baseURL) {
120    this.baseURL = baseURL;
121  }
122
123  public boolean isValidating() {
124    return validating;
125  }
126
127  public void setValidating(boolean validating) {
128    this.validating = validating;
129  }
130
131  public void execute() throws EGraphEngine, EGraphQLException, FHIRException {
132    assert services != null;
133    assert start != null;
134    assert bundle != null;
135    assert baseURL != null;
136    assert graphDefinition != null;
137    graphDefinition.checkNoModifiers("definition", "Building graph from GraphDefinition");
138
139    check(!start.fhirType().equals(graphDefinition.getStart()), "The Graph definition requires that the start (focus reosource) is "+graphDefinition.getStart()+", but instead found "+start.fhirType());
140    
141    if (!isInBundle(start)) {
142      addToBundle(start);
143    }
144    for (GraphDefinitionLinkComponent l : graphDefinition.getLink()) {
145      processLink(start.fhirType(), start, l, 1);
146    }
147  }
148
149  private void check(boolean b, String msg) {
150    if (!b) {
151      throw new FHIRException(msg);
152    }
153  }
154
155  private boolean isInBundle(Resource resource) {
156    for (BundleEntryComponent be : bundle.getEntry()) {
157      if (be.hasResource() && be.getResource().fhirType().equals(resource.fhirType()) && be.getResource().getId().equals(resource.getId())) {
158        return true;
159      }
160    }
161    return false;
162  }
163
164  private void addToBundle(Resource resource) {
165    BundleEntryComponent be = bundle.addEntry();
166    be.setFullUrl(Utilities.pathURL(baseURL, resource.fhirType(), resource.getId()));
167    be.setResource(resource);
168  }  
169
170  private void processLink(String focusPath, Resource focus, GraphDefinitionLinkComponent link, int depth) {
171    if (link.hasPath()) {
172      processLinkPath(focusPath, focus, link, depth);
173    } else {
174      processLinkTarget(focusPath, focus, link, depth);
175    }
176  }
177
178  private void processLinkPath(String focusPath, Resource focus, GraphDefinitionLinkComponent link, int depth) {
179    String path = focusPath+" -> "+link.getPath();
180    check(link.hasPath(), "Path is needed at "+path);
181    check(!link.hasSliceName(), "SliceName is not yet supported at "+path);
182    
183    ExpressionNode node;
184    if (link.getPathElement().hasUserData(TAG_NAME)) {
185        node = (ExpressionNode) link.getPathElement().getUserData(TAG_NAME);
186    } else {
187        node = engine.parse(link.getPath());
188        link.getPathElement().setUserData(TAG_NAME, node);
189    }
190    List<Base> matches = engine.evaluate(null, focus, focus, focus, node);
191    check(!validating || matches.size() >= (link.hasMin() ? link.getMin() : 0), "Link at path "+path+" requires at least "+link.getMin()+" matches, but only found "+matches.size());
192    check(!validating || matches.size() <= (link.hasMax() ?  Integer.parseInt(link.getMax()) : Integer.MAX_VALUE), "Link at path "+path+" requires at most "+link.getMax()+" matches, but found "+matches.size());
193//    for (Base sel : matches) {
194//      check(sel.fhirType().equals("Reference"), "Selected node from an expression must be a Reference"); // todo: should a URL be ok?
195//      ReferenceResolution res = services.lookup(appInfo, focus, (Reference) sel);
196//      if (res != null) {
197//        check(res.getTargetContext() != focus, "how to handle contained resources is not yet resolved"); // todo
198//        for (GraphDefinitionLinkTargetComponent tl : link.getTarget()) {
199//          if (tl.getType().equals(res.getTarget().fhirType())) {
200//            Resource r = (Resource) res.getTarget();
201//            if (!isInBundle(r)) {
202//              addToBundle(r);
203//              for (GraphDefinitionLinkComponent l : graphDefinition.getLink()) {
204//                processLink(focus.fhirType(), r, l, depth+1);
205//              }
206//            }
207//          }
208//        }
209//      }
210//    }
211  }
212  
213  private void processLinkTarget(String focusPath, Resource focus, GraphDefinitionLinkComponent link, int depth) {
214//    check(link.getTarget().size() == 1, "If there is no path, there must be one and only one target at "+focusPath);
215//    check(link.getTarget().get(0).hasType(), "If there is no path, there must be type on the target at "+focusPath);
216//    check(link.getTarget().get(0).getParams().contains("{ref}"), "If there is no path, the target must have parameters that include a parameter using {ref} at "+focusPath);
217//    String path = focusPath+" -> "+link.getTarget().get(0).getType()+"?"+link.getTarget().get(0).getParams();
218//    
219//    List<IBaseResource> list = new ArrayList<>();
220//    List<Argument> params = new ArrayList<>();
221//    parseParams(params, link.getTarget().get(0).getParams(), focus);
222//    services.listResources(appInfo, link.getTarget().get(0).getType().toCode(), params, list);
223//    check(!validating || (list.size() >= (link.hasMin() ? link.getMin() : 0)), "Link at path "+path+" requires at least "+link.getMin()+" matches, but only found "+list.size());
224//    check(!validating || (list.size() <= (link.hasMax() && !link.getMax().equals("*") ? Integer.parseInt(link.getMax()) : Integer.MAX_VALUE)), "Link at path "+path+" requires at most "+link.getMax()+" matches, but found "+list.size());
225//    for (IBaseResource res : list) {
226//      Resource r = (Resource) res;
227//      if (!isInBundle(r)) {
228//        addToBundle(r);
229//        // Grahame Grieve 17-06-2020: this seems wrong to me - why restart? 
230//        for (GraphDefinitionLinkComponent l : graphDefinition.getLink()) {
231//          processLink(start.fhirType(), start, l, depth+1);
232//        }
233//      }
234//    }
235  }
236
237    private void parseParams(List<Argument> params, String value, Resource res) {
238      boolean refed = false;
239      Map<String, List<String>> p = splitQuery(value);
240      for (String n : p.keySet()) {
241        for (String v : p.get(n)) {
242          if (v.equals("{ref}")) {
243            refed = true;
244            v = res.fhirType()+'/'+res.getId();
245          }
246          params.add(new Argument(n, new StringValue(v)));
247        }
248      }
249      check(refed, "no use of {ref} found");
250    }
251
252  public Map<String, List<String>> splitQuery(String string) {
253    final Map<String, List<String>> query_pairs = new LinkedHashMap<String, List<String>>();
254    final String[] pairs = string.split("&");
255    for (String pair : pairs) {
256      final int idx = pair.indexOf("=");
257      final String key = idx > 0 ? decode(pair.substring(0, idx), "UTF-8") : pair;
258      if (!query_pairs.containsKey(key)) {
259        query_pairs.put(key, new LinkedList<String>());
260      }
261      final String value = idx > 0 && pair.length() > idx + 1 ? decode(pair.substring(idx + 1), "UTF-8") : null;
262      query_pairs.get(key).add(value);
263    }
264    return query_pairs;
265  }
266
267  private String decode(String s, String enc) {
268    try {
269      return URLDecoder.decode(s, enc);
270    } catch (UnsupportedEncodingException e) {
271      return s;
272    }
273  }
274  
275}