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