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