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