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