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