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