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