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