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