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