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}