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