001package org.hl7.fhir.r4.utils; 002 003import java.io.UnsupportedEncodingException; 004import java.net.URLDecoder; 005import java.util.ArrayList; 006import java.util.HashMap; 007import java.util.LinkedHashMap; 008import java.util.LinkedList; 009import java.util.List; 010import java.util.Map; 011 012import org.hl7.fhir.utilities.graphql.Package; 013import org.hl7.fhir.utilities.graphql.Selection; 014import org.hl7.fhir.utilities.graphql.StringValue; 015import org.hl7.fhir.utilities.graphql.Value; 016import org.hl7.fhir.utilities.graphql.Variable; 017import org.hl7.fhir.utilities.graphql.VariableValue; 018import org.hl7.fhir.exceptions.FHIRException; 019import org.hl7.fhir.r4.context.IWorkerContext; 020import org.hl7.fhir.r4.model.BackboneElement; 021import org.hl7.fhir.r4.model.Base; 022import org.hl7.fhir.r4.model.Bundle; 023import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; 024import org.hl7.fhir.r4.model.Bundle.BundleLinkComponent; 025import org.hl7.fhir.r4.model.CanonicalType; 026import org.hl7.fhir.r4.model.DomainResource; 027import org.hl7.fhir.r4.model.Element; 028import org.hl7.fhir.r4.model.ExpressionNode; 029import org.hl7.fhir.r4.model.IntegerType; 030import org.hl7.fhir.r4.model.Property; 031import org.hl7.fhir.r4.model.Reference; 032import org.hl7.fhir.r4.model.Resource; 033import org.hl7.fhir.r4.model.StringType; 034import org.hl7.fhir.r4.model.StructureDefinition; 035import org.hl7.fhir.r4.utils.GraphQLEngine.SearchEdge; 036import org.hl7.fhir.r4.utils.GraphQLEngine.SearchWrapper; 037import org.hl7.fhir.r4.utils.FHIRLexer.FHIRLexerException; 038import org.hl7.fhir.r4.utils.GraphQLEngine.IGraphQLStorageServices.ReferenceResolution; 039import org.hl7.fhir.utilities.Utilities; 040import org.hl7.fhir.utilities.graphql.Argument; 041import org.hl7.fhir.utilities.graphql.Argument.ArgumentListStatus; 042import org.hl7.fhir.utilities.graphql.Directive; 043import org.hl7.fhir.utilities.graphql.EGraphEngine; 044import org.hl7.fhir.utilities.graphql.EGraphQLException; 045import org.hl7.fhir.utilities.graphql.Field; 046import org.hl7.fhir.utilities.graphql.Fragment; 047import org.hl7.fhir.utilities.graphql.NameValue; 048import org.hl7.fhir.utilities.graphql.NumberValue; 049import org.hl7.fhir.utilities.graphql.ObjectValue; 050import org.hl7.fhir.utilities.graphql.Operation; 051import org.hl7.fhir.utilities.graphql.Operation.OperationType; 052 053public class GraphQLEngine { 054 055 public class SearchEdge extends Base { 056 057 private BundleEntryComponent be; 058 private String type; 059 060 public SearchEdge(String type, BundleEntryComponent be) { 061 this.type = type; 062 this.be = be; 063 } 064 @Override 065 public String fhirType() { 066 return type; 067 } 068 069 @Override 070 protected void listChildren(List<Property> result) { 071 throw new Error("Not Implemented"); 072 } 073 074 @Override 075 public String getIdBase() { 076 throw new Error("Not Implemented"); 077 } 078 079 @Override 080 public void setIdBase(String value) { 081 throw new Error("Not Implemented"); 082 } 083 084 @Override 085 public Property getNamedProperty(int _hash, String _name, boolean _checkValid) throws FHIRException { 086 switch (_hash) { 087 case 3357091: /*mode*/ return new Property(_name, "string", "n/a", 0, 1, be.getSearch().hasMode() ? be.getSearch().getModeElement() : null); 088 case 109264530: /*score*/ return new Property(_name, "string", "n/a", 0, 1, be.getSearch().hasScore() ? be.getSearch().getScoreElement() : null); 089 case -341064690: /*resource*/ return new Property(_name, "resource", "n/a", 0, 1, be.hasResource() ? be.getResource() : null); 090 default: return super.getNamedProperty(_hash, _name, _checkValid); 091 } 092 } 093 } 094 095 public class SearchWrapper extends Base { 096 097 private Bundle bnd; 098 private String type; 099 private Map<String, String> map; 100 101 public SearchWrapper(String type, Bundle bnd) throws FHIRException { 102 this.type = type; 103 this.bnd = bnd; 104 for (BundleLinkComponent bl : bnd.getLink()) 105 if (bl.getRelation().equals("self")) 106 map = parseURL(bl.getUrl()); 107 } 108 109 @Override 110 public String fhirType() { 111 return type; 112 } 113 114 @Override 115 protected void listChildren(List<Property> result) { 116 throw new Error("Not Implemented"); 117 } 118 119 @Override 120 public String getIdBase() { 121 throw new Error("Not Implemented"); 122 } 123 124 @Override 125 public void setIdBase(String value) { 126 throw new Error("Not Implemented"); 127 } 128 129 @Override 130 public Property getNamedProperty(int _hash, String _name, boolean _checkValid) throws FHIRException { 131 switch (_hash) { 132 case 97440432: /*first*/ return new Property(_name, "string", "n/a", 0, 1, extractLink(_name)); 133 case -1273775369: /*previous*/ return new Property(_name, "string", "n/a", 0, 1, extractLink(_name)); 134 case 3377907: /*next*/ return new Property(_name, "string", "n/a", 0, 1, extractLink(_name)); 135 case 3314326: /*last*/ return new Property(_name, "string", "n/a", 0, 1, extractLink(_name)); 136 case 94851343: /*count*/ return new Property(_name, "integer", "n/a", 0, 1, bnd.getTotalElement()); 137 case -1019779949:/*offset*/ return new Property(_name, "integer", "n/a", 0, 1, extractParam("search-offset")); 138 case 860381968: /*pagesize*/ return new Property(_name, "integer", "n/a", 0, 1, extractParam("_count")); 139 case 96356950: /*edges*/ return new Property(_name, "edge", "n/a", 0, Integer.MAX_VALUE, getEdges()); 140 default: return super.getNamedProperty(_hash, _name, _checkValid); 141 } 142 } 143 144 private List<Base> getEdges() { 145 List<Base> list = new ArrayList<>(); 146 for (BundleEntryComponent be : bnd.getEntry()) 147 list.add(new SearchEdge(type.substring(0, type.length()-10)+"Edge", be)); 148 return list; 149 } 150 151 private Base extractParam(String name) throws FHIRException { 152 return map != null ? new IntegerType(map.get(name)) : null; 153 } 154 155 private Map<String, String> parseURL(String url) throws FHIRException { 156 try { 157 Map<String, String> map = new HashMap<String, String>(); 158 String[] pairs = url.split("&"); 159 for (String pair : pairs) { 160 int idx = pair.indexOf("="); 161 String key; 162 key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair; 163 String value = idx > 0 && pair.length() > idx + 1 ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null; 164 map.put(key, value); 165 } 166 return map; 167 } catch (UnsupportedEncodingException e) { 168 throw new FHIRException(e); 169 } 170 } 171 172 private Base extractLink(String _name) throws FHIRException { 173 for (BundleLinkComponent bl : bnd.getLink()) { 174 if (bl.getRelation().equals(_name)) { 175 Map<String, String> map = parseURL(bl.getUrl()); 176 return new StringType(map.get("search-id")+':'+map.get("search-offset")); 177 } 178 } 179 return null; 180 } 181 182 } 183 184 public interface IGraphQLStorageServices { 185 public class ReferenceResolution { 186 private Resource targetContext; 187 private Resource target; 188 public ReferenceResolution(Resource targetContext, Resource target) { 189 super(); 190 this.targetContext = targetContext; 191 this.target = target; 192 } 193 194 195 } 196 // given a reference inside a context, return what it references (including resolving internal references (e.g. start with #) 197 public ReferenceResolution lookup(Object appInfo, Resource context, Reference reference) throws FHIRException; 198 199 // just get the identified resource 200 public Resource lookup(Object appInfo, String type, String id) throws FHIRException; 201 202 // list the matching resources. searchParams are the standard search params. 203 // this instanceof different to search because the server returns all matching resources, or an error. There instanceof no paging on this search 204 public void listResources(Object appInfo, String type, List<Argument> searchParams, List<Resource> matches) throws FHIRException; 205 206 // just perform a standard search, and return the bundle as you return to the client 207 public Bundle search(Object appInfo, String type, List<Argument> searchParams) throws FHIRException; 208 } 209 210 private IWorkerContext context; 211 212 public GraphQLEngine(IWorkerContext context) { 213 super(); 214 this.context = context; 215 } 216 217 /** 218 * for the host to pass context into and get back on the reference resolution interface 219 */ 220 private Object appInfo; 221 222 /** 223 * the focus resource - if (there instanceof one. if (there isn"t,) there instanceof no focus 224 */ 225 private Resource focus; 226 227 /** 228 * The package that describes the graphQL to be executed, operation name, and variables 229 */ 230 private Package graphQL; 231 232 /** 233 * where the output from executing the query instanceof going to go 234 */ 235 private ObjectValue output; 236 237 /** 238 * Application provided reference resolution services 239 */ 240 private IGraphQLStorageServices services; 241 242 // internal stuff 243 private Map<String, Argument> workingVariables = new HashMap<String, Argument>(); 244 245 private FHIRPathEngine fpe; 246 247 private ExpressionNode magicExpression; 248 249 public void execute() throws EGraphEngine, EGraphQLException, FHIRException { 250 if (graphQL == null) 251 throw new EGraphEngine("Unable to process graphql - graphql document missing"); 252 fpe = new FHIRPathEngine(this.context); 253 magicExpression = new ExpressionNode(0); 254 255 output = new ObjectValue(); 256 257 Operation op = null; 258 // todo: initial conditions 259 if (!Utilities.noString(graphQL.getOperationName())) { 260 op = graphQL.getDocument().operation(graphQL.getOperationName()); 261 if (op == null) 262 throw new EGraphEngine("Unable to find operation \""+graphQL.getOperationName()+"\""); 263 } else if ((graphQL.getDocument().getOperations().size() == 1)) 264 op = graphQL.getDocument().getOperations().get(0); 265 else 266 throw new EGraphQLException("No operation name provided, so expected to find a single operation"); 267 268 if (op.getOperationType() == OperationType.qglotMutation) 269 throw new EGraphQLException("Mutation operations are not supported (yet)"); 270 271 checkNoDirectives(op.getDirectives()); 272 processVariables(op); 273 if (focus == null) 274 processSearch(output, op.getSelectionSet(), false, ""); 275 else 276 processObject(focus, focus, output, op.getSelectionSet(), false, ""); 277 } 278 279 private boolean checkBooleanDirective(Directive dir) throws EGraphQLException { 280 if (dir.getArguments().size() != 1) 281 throw new EGraphQLException("Unable to process @"+dir.getName()+": expected a single argument \"if\""); 282 if (!dir.getArguments().get(0).getName().equals("if")) 283 throw new EGraphQLException("Unable to process @"+dir.getName()+": expected a single argument \"if\""); 284 List<Value> vl = resolveValues(dir.getArguments().get(0), 1); 285 return vl.get(0).toString().equals("true"); 286 } 287 288 private boolean checkDirectives(List<Directive> directives) throws EGraphQLException { 289 Directive skip = null; 290 Directive include = null; 291 for (Directive dir : directives) { 292 if (dir.getName().equals("skip")) { 293 if ((skip == null)) 294 skip = dir; 295 else 296 throw new EGraphQLException("Duplicate @skip directives"); 297 } else if (dir.getName().equals("include")) { 298 if ((include == null)) 299 include = dir; 300 else 301 throw new EGraphQLException("Duplicate @include directives"); 302 } 303 else if (!Utilities.existsInList(dir.getName(), "flatten", "first", "singleton", "slice")) 304 throw new EGraphQLException("Directive \""+dir.getName()+"\" instanceof not recognised"); 305 } 306 if ((skip != null && include != null)) 307 throw new EGraphQLException("Cannot mix @skip and @include directives"); 308 if (skip != null) 309 return !checkBooleanDirective(skip); 310 else if (include != null) 311 return checkBooleanDirective(include); 312 else 313 return true; 314 } 315 316 private void checkNoDirectives(List<Directive> directives) { 317 318 } 319 320 private boolean targetTypeOk(List<Argument> arguments, Resource dest) throws EGraphQLException { 321 List<String> list = new ArrayList<String>(); 322 for (Argument arg : arguments) { 323 if ((arg.getName().equals("type"))) { 324 List<Value> vl = resolveValues(arg); 325 for (Value v : vl) 326 list.add(v.toString()); 327 } 328 } 329 if (list.size() == 0) 330 return true; 331 else 332 return list.indexOf(dest.fhirType()) > -1; 333 } 334 335 private boolean hasExtensions(Base obj) { 336 if (obj instanceof BackboneElement) 337 return ((BackboneElement) obj).getExtension().size() > 0 || ((BackboneElement) obj).getModifierExtension().size() > 0; 338 else if (obj instanceof DomainResource) 339 return ((DomainResource)obj).getExtension().size() > 0 || ((DomainResource)obj).getModifierExtension().size() > 0; 340 else if (obj instanceof Element) 341 return ((Element)obj).getExtension().size() > 0; 342 else 343 return false; 344 } 345 346 private boolean passesExtensionMode(Base obj, boolean extensionMode) { 347 if (!obj.isPrimitive()) 348 return !extensionMode; 349 else if (extensionMode) 350 return !Utilities.noString(obj.getIdBase()) || hasExtensions(obj); 351 else 352 return obj.primitiveValue() != ""; 353 } 354 355 private List<Base> filter(Resource context, Property prop, List<Argument> arguments, List<Base> values, boolean extensionMode) throws FHIRException, EGraphQLException { 356 List<Base> result = new ArrayList<Base>(); 357 if (values.size() > 0) { 358 int count = Integer.MAX_VALUE; 359 int offset = 0; 360 StringBuilder fp = new StringBuilder(); 361 for (Argument arg : arguments) { 362 List<Value> vl = resolveValues(arg); 363 if ((vl.size() != 1)) 364 throw new EGraphQLException("Incorrect number of arguments"); 365 if (values.get(0).isPrimitive()) 366 throw new EGraphQLException("Attempt to use a filter ("+arg.getName()+") on a primtive type ("+prop.getTypeCode()+")"); 367 if ((arg.getName().equals("fhirpath"))) 368 fp.append(" and "+vl.get(0).toString()); 369 else if ((arg.getName().equals("_count"))) 370 count = Integer.valueOf(vl.get(0).toString()); 371 else if ((arg.getName().equals("_offset"))) 372 offset = Integer.valueOf(vl.get(0).toString()); 373 else { 374 Property p = values.get(0).getNamedProperty(arg.getName()); 375 if (p == null) 376 throw new EGraphQLException("Attempt to use an unknown filter ("+arg.getName()+") on a type ("+prop.getTypeCode()+")"); 377 fp.append(" and "+arg.getName()+" = '"+vl.get(0).toString()+"'"); 378 } 379 } 380 int i = 0; 381 int t = 0; 382 if (fp.length() == 0) 383 for (Base v : values) { 384 if ((i >= offset) && passesExtensionMode(v, extensionMode)) { 385 result.add(v); 386 t++; 387 if (t >= count) 388 break; 389 } 390 i++; 391 } else { 392 ExpressionNode node = fpe.parse(fp.toString().substring(5)); 393 for (Base v : values) { 394 if ((i >= offset) && passesExtensionMode(v, extensionMode) && fpe.evaluateToBoolean(null, context, v, node)) { 395 result.add(v); 396 t++; 397 if (t >= count) 398 break; 399 } 400 i++; 401 } 402 } 403 } 404 return result; 405 } 406 407 private List<Resource> filterResources(Argument fhirpath, Bundle bnd) throws EGraphQLException, FHIRException { 408 List<Resource> result = new ArrayList<Resource>(); 409 if (bnd.getEntry().size() > 0) { 410 if ((fhirpath == null)) 411 for (BundleEntryComponent be : bnd.getEntry()) 412 result.add(be.getResource()); 413 else { 414 FHIRPathEngine fpe = new FHIRPathEngine(context); 415 ExpressionNode node = fpe.parse(getSingleValue(fhirpath)); 416 for (BundleEntryComponent be : bnd.getEntry()) 417 if (fpe.evaluateToBoolean(null, be.getResource(), be.getResource(), node)) 418 result.add(be.getResource()); 419 } 420 } 421 return result; 422 } 423 424 private List<Resource> filterResources(Argument fhirpath, List<Resource> list) throws EGraphQLException, FHIRException { 425 List<Resource> result = new ArrayList<Resource>(); 426 if (list.size() > 0) { 427 if ((fhirpath == null)) 428 for (Resource v : list) 429 result.add(v); 430 else { 431 FHIRPathEngine fpe = new FHIRPathEngine(context); 432 ExpressionNode node = fpe.parse(getSingleValue(fhirpath)); 433 for (Resource v : list) 434 if (fpe.evaluateToBoolean(null, v, v, node)) 435 result.add(v); 436 } 437 } 438 return result; 439 } 440 441 private boolean hasArgument(List<Argument> arguments, String name, String value) { 442 for (Argument arg : arguments) 443 if ((arg.getName().equals(name)) && arg.hasValue(value)) 444 return true; 445 return false; 446 } 447 448 private void processValues(Resource context, Selection sel, Property prop, ObjectValue target, List<Base> values, boolean extensionMode, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 449 boolean il = false; 450 Argument arg = null; 451 ExpressionNode expression = null; 452 if (sel.getField().hasDirective("slice")) { 453 Directive dir = sel.getField().directive("slice"); 454 String s = ((StringValue) dir.getArguments().get(0).getValues().get(0)).getValue(); 455 if (s.equals("$index")) 456 expression = magicExpression; 457 else 458 expression = fpe.parse(s); 459 } 460 if (sel.getField().hasDirective("flatten")) // special: instruction to drop this node... 461 il = prop.isList() && !sel.getField().hasDirective("first"); 462 else if (sel.getField().hasDirective("first")) { 463 if (expression != null) 464 throw new FHIRException("You cannot mix @slice and @first"); 465 arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), inheritedList)); 466 } else if (expression == null) 467 arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), prop.isList() || inheritedList)); 468 469 470 int index = 0; 471 for (Base value : values) { 472 String ss = ""; 473 if (expression != null) { 474 if (expression == magicExpression) 475 ss = suffix+'.'+Integer.toString(index); 476 else 477 ss = suffix+'.'+fpe.evaluateToString(null, null, value, expression); 478 if (!sel.getField().hasDirective("flatten")) 479 arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), prop.isList() || inheritedList)); 480 } 481 482 if (value.isPrimitive() && !extensionMode) { 483 if (!sel.getField().getSelectionSet().isEmpty()) 484 throw new EGraphQLException("Encountered a selection set on a scalar field type"); 485 processPrimitive(arg, value); 486 } else { 487 if (sel.getField().getSelectionSet().isEmpty()) 488 throw new EGraphQLException("No Fields selected on a complex object"); 489 if (arg == null) 490 processObject(context, value, target, sel.getField().getSelectionSet(), il, ss); 491 else { 492 ObjectValue n = new ObjectValue(); 493 arg.addValue(n); 494 processObject(context, value, n, sel.getField().getSelectionSet(), il, ss); 495 } 496 } 497 if (sel.getField().hasDirective("first")) 498 return; 499 index++; 500 } 501 } 502 503 private void processVariables(Operation op) throws EGraphQLException { 504 for (Variable varRef : op.getVariables()) { 505 Argument varDef = null; 506 for (Argument v : graphQL.getVariables()) 507 if (v.getName().equals(varRef.getName())) 508 varDef = v; 509 if (varDef != null) 510 workingVariables.put(varRef.getName(), varDef); // todo: check type? 511 else if (varRef.getDefaultValue() != null) 512 workingVariables.put(varRef.getName(), new Argument(varRef.getName(), varRef.getDefaultValue())); 513 else 514 throw new EGraphQLException("No value found for variable "); 515 } 516 } 517 518 private boolean isPrimitive(String typename) { 519 return Utilities.existsInList(typename, "boolean", "integer", "string", "decimal", "uri", "base64Binary", "instant", "date", "dateTime", "time", "code", "oid", "id", "markdown", "unsignedInt", "positiveInt", "url", "canonical"); 520 } 521 522 private boolean isResourceName(String name, String suffix) { 523 if (!name.endsWith(suffix)) 524 return false; 525 name = name.substring(0, name.length()-suffix.length()); 526 return context.getResourceNamesAsSet().contains(name); 527 } 528 529 private void processObject(Resource context, Base source, ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 530 for (Selection sel : selection) { 531 if (sel.getField() != null) { 532 if (checkDirectives(sel.getField().getDirectives())) { 533 Property prop = source.getNamedProperty(sel.getField().getName()); 534 if ((prop == null) && sel.getField().getName().startsWith("_")) 535 prop = source.getNamedProperty(sel.getField().getName().substring(1)); 536 if (prop == null) { 537 if ((sel.getField().getName().equals("resourceType") && source instanceof Resource)) 538 target.addField("resourceType", listStatus(sel.getField(), false)).addValue(new StringValue(source.fhirType())); 539 else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("Reference"))) 540 processReference(context, source, sel.getField(), target, inheritedList, suffix); 541 else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("canonical"))) 542 processCanonicalReference(context, source, sel.getField(), target, inheritedList, suffix); 543 else if (isResourceName(sel.getField().getName(), "List") && (source instanceof Resource)) 544 processReverseReferenceList((Resource) source, sel.getField(), target, inheritedList, suffix); 545 else if (isResourceName(sel.getField().getName(), "Connection") && (source instanceof Resource)) 546 processReverseReferenceSearch((Resource) source, sel.getField(), target, inheritedList, suffix); 547 else 548 throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType()); 549 } else { 550 if (!isPrimitive(prop.getTypeCode()) && sel.getField().getName().startsWith("_")) 551 throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType()); 552 553 List<Base> vl = filter(context, prop, sel.getField().getArguments(), prop.getValues(), sel.getField().getName().startsWith("_")); 554 if (!vl.isEmpty()) 555 processValues(context, sel, prop, target, vl, sel.getField().getName().startsWith("_"), inheritedList, suffix); 556 } 557 } 558 } else if (sel.getInlineFragment() != null) { 559 if (checkDirectives(sel.getInlineFragment().getDirectives())) { 560 if (Utilities.noString(sel.getInlineFragment().getTypeCondition())) 561 throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid? 562 if (source.fhirType().equals(sel.getInlineFragment().getTypeCondition())) 563 processObject(context, source, target, sel.getInlineFragment().getSelectionSet(), inheritedList, suffix); 564 } 565 } else if (checkDirectives(sel.getFragmentSpread().getDirectives())) { 566 Fragment fragment = graphQL.getDocument().fragment(sel.getFragmentSpread().getName()); 567 if (fragment == null) 568 throw new EGraphQLException("Unable to resolve fragment "+sel.getFragmentSpread().getName()); 569 570 if (Utilities.noString(fragment.getTypeCondition())) 571 throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid? 572 if (source.fhirType().equals(fragment.getTypeCondition())) 573 processObject(context, source, target, fragment.getSelectionSet(), inheritedList, suffix); 574 } 575 } 576 } 577 578 private void processPrimitive(Argument arg, Base value) { 579 String s = value.fhirType(); 580 if (s.equals("integer") || s.equals("decimal") || s.equals("unsignedInt") || s.equals("positiveInt")) 581 arg.addValue(new NumberValue(value.primitiveValue())); 582 else if (s.equals("boolean")) 583 arg.addValue(new NameValue(value.primitiveValue())); 584 else 585 arg.addValue(new StringValue(value.primitiveValue())); 586 } 587 588 private void processReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 589 if (!(source instanceof Reference)) 590 throw new EGraphQLException("Not done yet"); 591 if (services == null) 592 throw new EGraphQLException("Resource Referencing services not provided"); 593 594 Reference ref = (Reference) source; 595 ReferenceResolution res = services.lookup(appInfo, context, ref); 596 if (res != null) { 597 if (targetTypeOk(field.getArguments(), res.target)) { 598 Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList)); 599 ObjectValue obj = new ObjectValue(); 600 arg.addValue(obj); 601 processObject(res.targetContext, res.target, obj, field.getSelectionSet(), inheritedList, suffix); 602 } 603 } 604 else if (!hasArgument(field.getArguments(), "optional", "true")) 605 throw new EGraphQLException("Unable to resolve reference to "+ref.getReference()); 606 } 607 608 private void processCanonicalReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 609 if (!(source instanceof CanonicalType)) 610 throw new EGraphQLException("Not done yet"); 611 if (services == null) 612 throw new EGraphQLException("Resource Referencing services not provided"); 613 614 Reference ref = new Reference(source.primitiveValue()); 615 ReferenceResolution res = services.lookup(appInfo, context, ref); 616 if (res != null) { 617 if (targetTypeOk(field.getArguments(), res.target)) { 618 Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList)); 619 ObjectValue obj = new ObjectValue(); 620 arg.addValue(obj); 621 processObject(res.targetContext, res.target, obj, field.getSelectionSet(), inheritedList, suffix); 622 } 623 } 624 else if (!hasArgument(field.getArguments(), "optional", "true")) 625 throw new EGraphQLException("Unable to resolve reference to "+ref.getReference()); 626 } 627 628 private ArgumentListStatus listStatus(Field field, boolean isList) { 629 if (field.hasDirective("singleton")) 630 return ArgumentListStatus.SINGLETON; 631 else if (isList) 632 return ArgumentListStatus.REPEATING; 633 else 634 return ArgumentListStatus.NOT_SPECIFIED; 635 } 636 637 private void processReverseReferenceList(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 638 if (services == null) 639 throw new EGraphQLException("Resource Referencing services not provided"); 640 List<Resource> list = new ArrayList<Resource>(); 641 List<Argument> params = new ArrayList<Argument>(); 642 Argument parg = null; 643 for (Argument a : field.getArguments()) 644 if (!(a.getName().equals("_reference"))) 645 params.add(a); 646 else if ((parg == null)) 647 parg = a; 648 else 649 throw new EGraphQLException("Duplicate parameter _reference"); 650 if (parg == null) 651 throw new EGraphQLException("Missing parameter _reference"); 652 Argument arg = new Argument(); 653 params.add(arg); 654 arg.setName(getSingleValue(parg)); 655 arg.addValue(new StringValue(source.fhirType()+"/"+source.getId())); 656 services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), params, list); 657 arg = null; 658 ObjectValue obj = null; 659 660 List<Resource> vl = filterResources(field.argument("fhirpath"), list); 661 if (!vl.isEmpty()) { 662 arg = target.addField(field.getAlias()+suffix, listStatus(field, true)); 663 for (Resource v : vl) { 664 obj = new ObjectValue(); 665 arg.addValue(obj); 666 processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix); 667 } 668 } 669 } 670 671 private void processReverseReferenceSearch(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 672 if (services == null) 673 throw new EGraphQLException("Resource Referencing services not provided"); 674 List<Argument> params = new ArrayList<Argument>(); 675 Argument parg = null; 676 for (Argument a : field.getArguments()) 677 if (!(a.getName().equals("_reference"))) 678 params.add(a); 679 else if ((parg == null)) 680 parg = a; 681 else 682 throw new EGraphQLException("Duplicate parameter _reference"); 683 if (parg == null) 684 throw new EGraphQLException("Missing parameter _reference"); 685 Argument arg = new Argument(); 686 params.add(arg); 687 arg.setName(getSingleValue(parg)); 688 arg.addValue(new StringValue(source.fhirType()+"/"+source.getId())); 689 Bundle bnd = services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params); 690 Base bndWrapper = new SearchWrapper(field.getName(), bnd); 691 arg = target.addField(field.getAlias()+suffix, listStatus(field, false)); 692 ObjectValue obj = new ObjectValue(); 693 arg.addValue(obj); 694 processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix); 695 } 696 697 private void processSearch(ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 698 for (Selection sel : selection) { 699 if ((sel.getField() == null)) 700 throw new EGraphQLException("Only field selections are allowed in this context"); 701 checkNoDirectives(sel.getField().getDirectives()); 702 703 if ((isResourceName(sel.getField().getName(), ""))) 704 processSearchSingle(target, sel.getField(), inheritedList, suffix); 705 else if ((isResourceName(sel.getField().getName(), "List"))) 706 processSearchSimple(target, sel.getField(), inheritedList, suffix); 707 else if ((isResourceName(sel.getField().getName(), "Connection"))) 708 processSearchFull(target, sel.getField(), inheritedList, suffix); 709 } 710 } 711 712 private void processSearchSingle(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 713 if (services == null) 714 throw new EGraphQLException("Resource Referencing services not provided"); 715 String id = ""; 716 for (Argument arg : field.getArguments()) 717 if ((arg.getName().equals("id"))) 718 id = getSingleValue(arg); 719 else 720 throw new EGraphQLException("Unknown/invalid parameter "+arg.getName()); 721 if (Utilities.noString(id)) 722 throw new EGraphQLException("No id found"); 723 Resource res = services.lookup(appInfo, field.getName(), id); 724 if (res == null) 725 throw new EGraphQLException("Resource "+field.getName()+"/"+id+" not found"); 726 Argument arg = target.addField(field.getAlias()+suffix, listStatus(field, false)); 727 ObjectValue obj = new ObjectValue(); 728 arg.addValue(obj); 729 processObject(res, res, obj, field.getSelectionSet(), inheritedList, suffix); 730 } 731 732 private void processSearchSimple(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 733 if (services == null) 734 throw new EGraphQLException("Resource Referencing services not provided"); 735 List<Resource> list = new ArrayList<Resource>(); 736 services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), field.getArguments(), list); 737 Argument arg = null; 738 ObjectValue obj = null; 739 740 List<Resource> vl = filterResources(field.argument("fhirpath"), list); 741 if (!vl.isEmpty()) { 742 arg = target.addField(field.getAlias()+suffix, listStatus(field, true)); 743 for (Resource v : vl) { 744 obj = new ObjectValue(); 745 arg.addValue(obj); 746 processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix); 747 } 748 } 749 } 750 751 private void processSearchFull(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 752 if (services == null) 753 throw new EGraphQLException("Resource Referencing services not provided"); 754 List<Argument> params = new ArrayList<Argument>(); 755 Argument carg = null; 756 for ( Argument arg : field.getArguments()) 757 if (arg.getName().equals("cursor")) 758 carg = arg; 759 else 760 params.add(arg); 761 if ((carg != null)) { 762 params.clear();; 763 String[] parts = getSingleValue(carg).split(":"); 764 params.add(new Argument("search-id", new StringValue(parts[0]))); 765 params.add(new Argument("search-offset", new StringValue(parts[1]))); 766 } 767 768 Bundle bnd = services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params); 769 SearchWrapper bndWrapper = new SearchWrapper(field.getName(), bnd); 770 Argument arg = target.addField(field.getAlias()+suffix, listStatus(field, false)); 771 ObjectValue obj = new ObjectValue(); 772 arg.addValue(obj); 773 processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix); 774 } 775 776 private String getSingleValue(Argument arg) throws EGraphQLException { 777 List<Value> vl = resolveValues(arg, 1); 778 if (vl.size() == 0) 779 return ""; 780 return vl.get(0).toString(); 781 } 782 783 private List<Value> resolveValues(Argument arg) throws EGraphQLException { 784 return resolveValues(arg, -1, ""); 785 } 786 787 private List<Value> resolveValues(Argument arg, int max) throws EGraphQLException { 788 return resolveValues(arg, max, ""); 789 } 790 791 private List<Value> resolveValues(Argument arg, int max, String vars) throws EGraphQLException { 792 List<Value> result = new ArrayList<Value>(); 793 for (Value v : arg.getValues()) { 794 if (! (v instanceof VariableValue)) 795 result.add(v); 796 else { 797 if (vars.contains(":"+v.toString()+":")) 798 throw new EGraphQLException("Recursive reference to variable "+v.toString()); 799 Argument a = workingVariables.get(v.toString()); 800 if (a == null) 801 throw new EGraphQLException("No value found for variable \""+v.toString()+"\" in \""+arg.getName()+"\""); 802 List<Value> vl = resolveValues(a, -1, vars+":"+v.toString()+":"); 803 result.addAll(vl); 804 } 805 } 806 if ((max != -1 && result.size() > max)) 807 throw new EGraphQLException("Only "+Integer.toString(max)+" values are allowed for \""+arg.getName()+"\", but "+Integer.toString(result.size())+" enoucntered"); 808 return result; 809 } 810 811 812 813 814 public Object getAppInfo() { 815 return appInfo; 816 } 817 818 public void setAppInfo(Object appInfo) { 819 this.appInfo = appInfo; 820 } 821 822 public Resource getFocus() { 823 return focus; 824 } 825 826 public void setFocus(Resource focus) { 827 this.focus = focus; 828 } 829 830 public Package getGraphQL() { 831 return graphQL; 832 } 833 834 public void setGraphQL(Package graphQL) { 835 this.graphQL = graphQL; 836 } 837 838 public ObjectValue getOutput() { 839 return output; 840 } 841 842 public IGraphQLStorageServices getServices() { 843 return services; 844 } 845 846 public void setServices(IGraphQLStorageServices services) { 847 this.services = services; 848 } 849 850 851 // 852//{ GraphQLSearchWrapper } 853// 854//constructor GraphQLSearchWrapper.Create(bundle : Bundle); 855//var 856// s : String; 857//{ 858// inherited Create; 859// FBundle = bundle; 860// s = bundle_List.Matches["self"]; 861// FParseMap = TParseMap.create(s.Substring(s.IndexOf("?")+1)); 862//} 863// 864//destructor GraphQLSearchWrapper.Destroy; 865//{ 866// FParseMap.free; 867// FBundle.Free; 868// inherited; 869//} 870// 871//function GraphQLSearchWrapper.extractLink(name: String): String; 872//var 873// s : String; 874// pm : TParseMap; 875//{ 876// s = FBundle_List.Matches[name]; 877// if (s == "") 878// result = null 879// else 880// { 881// pm = TParseMap.create(s.Substring(s.IndexOf("?")+1)); 882// try 883// result = String.Create(pm.GetVar("search-id")+":"+pm.GetVar("search-offset")); 884// finally 885// pm.Free; 886// } 887// } 888//} 889// 890//function GraphQLSearchWrapper.extractParam(name: String; int : boolean): Base; 891//var 892// s : String; 893//{ 894// s = FParseMap.GetVar(name); 895// if (s == "") 896// result = null 897// else if (int) 898// result = Integer.Create(s) 899// else 900// result = String.Create(s); 901//} 902// 903//function GraphQLSearchWrapper.fhirType(): String; 904//{ 905// result = "*Connection"; 906//} 907// 908// // http://test.fhir.org/r4/Patient?_format==text/xhtml&search-id==77c97e03-8a6c-415f-a63d-11c80cf73f&&active==true&_sort==_id&search-offset==50&_count==50 909// 910//function GraphQLSearchWrapper.getPropertyValue(propName: string): Property; 911//var 912// list : List<GraphQLSearchEdge>; 913// be : BundleEntry; 914//{ 915// if (propName == "first") 916// result = Property.Create(self, propname, "string", false, String, extractLink("first")) 917// else if (propName == "previous") 918// result = Property.Create(self, propname, "string", false, String, extractLink("previous")) 919// else if (propName == "next") 920// result = Property.Create(self, propname, "string", false, String, extractLink("next")) 921// else if (propName == "last") 922// result = Property.Create(self, propname, "string", false, String, extractLink("last")) 923// else if (propName == "count") 924// result = Property.Create(self, propname, "integer", false, String, FBundle.totalElement) 925// else if (propName == "offset") 926// result = Property.Create(self, propname, "integer", false, Integer, extractParam("search-offset", true)) 927// else if (propName == "pagesize") 928// result = Property.Create(self, propname, "integer", false, Integer, extractParam("_count", true)) 929// else if (propName == "edges") 930// { 931// list = ArrayList<GraphQLSearchEdge>(); 932// try 933// for be in FBundle.getEntry() do 934// list.add(GraphQLSearchEdge.create(be)); 935// result = Property.Create(self, propname, "integer", true, Integer, List<Base>(list)); 936// finally 937// list.Free; 938// } 939// } 940// else 941// result = null; 942//} 943// 944//private void GraphQLSearchWrapper.SetBundle(const Value: Bundle); 945//{ 946// FBundle.Free; 947// FBundle = Value; 948//} 949// 950//{ GraphQLSearchEdge } 951// 952//constructor GraphQLSearchEdge.Create(entry: BundleEntry); 953//{ 954// inherited Create; 955// FEntry = entry; 956//} 957// 958//destructor GraphQLSearchEdge.Destroy; 959//{ 960// FEntry.Free; 961// inherited; 962//} 963// 964//function GraphQLSearchEdge.fhirType(): String; 965//{ 966// result = "*Edge"; 967//} 968// 969//function GraphQLSearchEdge.getPropertyValue(propName: string): Property; 970//{ 971// if (propName == "mode") 972// { 973// if (FEntry.search != null) 974// result = Property.Create(self, propname, "code", false, Enum, FEntry.search.modeElement) 975// else 976// result = Property.Create(self, propname, "code", false, Enum, Base(null)); 977// } 978// else if (propName == "score") 979// { 980// if (FEntry.search != null) 981// result = Property.Create(self, propname, "decimal", false, Decimal, FEntry.search.scoreElement) 982// else 983// result = Property.Create(self, propname, "decimal", false, Decimal, Base(null)); 984// } 985// else if (propName == "resource") 986// result = Property.Create(self, propname, "resource", false, Resource, FEntry.getResource()) 987// else 988// result = null; 989//} 990// 991//private void GraphQLSearchEdge.SetEntry(const Value: BundleEntry); 992//{ 993// FEntry.Free; 994// FEntry = value; 995//} 996// 997}