
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}