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