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