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