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.model.BackboneElement; 045import org.hl7.fhir.r5.model.Base; 046import org.hl7.fhir.r5.model.Bundle; 047import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent; 048import org.hl7.fhir.r5.model.Bundle.BundleLinkComponent; 049import org.hl7.fhir.r5.model.CanonicalType; 050import org.hl7.fhir.r5.model.DomainResource; 051import org.hl7.fhir.r5.model.Element; 052import org.hl7.fhir.r5.model.ExpressionNode; 053import org.hl7.fhir.r5.model.IntegerType; 054import org.hl7.fhir.r5.model.Property; 055import org.hl7.fhir.r5.model.Reference; 056import org.hl7.fhir.r5.model.Resource; 057import org.hl7.fhir.r5.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, String fieldName, 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 385 // Account for situations where the GraphQL expression selected e.g. 386 // effectiveDateTime but the field contains effectivePeriod 387 String propName = prop.getName(); 388 List<Base> newValues = new ArrayList<>(values.size()); 389 for (Base value : values) { 390 if (propName.endsWith("[x]")) { 391 String propNameShortened = propName.substring(0, propName.length() - 3); 392 if (fieldName.startsWith(propNameShortened) && fieldName.length() > propNameShortened.length()) { 393 if (!value.fhirType().equalsIgnoreCase(fieldName.substring(propNameShortened.length()))) { 394 continue; 395 } 396 } 397 } 398 newValues.add(value); 399 } 400 401 int i = 0; 402 int t = 0; 403 if (fp.length() == 0) 404 for (Base v : newValues) { 405 if ((i >= offset) && passesExtensionMode(v, extensionMode)) { 406 result.add(v); 407 t++; 408 if (t >= count) 409 break; 410 } 411 i++; 412 } else { 413 ExpressionNode node = fpe.parse(fp.substring(5)); 414 for (Base v : newValues) { 415 if ((i >= offset) && passesExtensionMode(v, extensionMode) && fpe.evaluateToBoolean(null, context, v, node)) { 416 result.add(v); 417 t++; 418 if (t >= count) 419 break; 420 } 421 i++; 422 } 423 } 424 } 425 return result; 426 } 427 428 private List<Resource> filterResources(Argument fhirpath, Bundle bnd) throws EGraphQLException, FHIRException { 429 List<Resource> result = new ArrayList<Resource>(); 430 if (bnd.getEntry().size() > 0) { 431 if ((fhirpath == null)) 432 for (BundleEntryComponent be : bnd.getEntry()) 433 result.add(be.getResource()); 434 else { 435 FHIRPathEngine fpe = new FHIRPathEngine(context); 436 ExpressionNode node = fpe.parse(getSingleValue(fhirpath)); 437 for (BundleEntryComponent be : bnd.getEntry()) 438 if (fpe.evaluateToBoolean(null, be.getResource(), be.getResource(), node)) 439 result.add(be.getResource()); 440 } 441 } 442 return result; 443 } 444 445 private List<Resource> filterResources(Argument fhirpath, List<IBaseResource> list) throws EGraphQLException, FHIRException { 446 List<Resource> result = new ArrayList<Resource>(); 447 if (list.size() > 0) { 448 if ((fhirpath == null)) 449 for (IBaseResource v : list) 450 result.add((Resource) v); 451 else { 452 FHIRPathEngine fpe = new FHIRPathEngine(context); 453 ExpressionNode node = fpe.parse(getSingleValue(fhirpath)); 454 for (IBaseResource v : list) 455 if (fpe.evaluateToBoolean(null, (Resource)v, (Base) v, node)) 456 result.add((Resource) v); 457 } 458 } 459 return result; 460 } 461 462 private boolean hasArgument(List<Argument> arguments, String name, String value) { 463 for (Argument arg : arguments) 464 if ((arg.getName().equals(name)) && arg.hasValue(value)) 465 return true; 466 return false; 467 } 468 469 private void processValues(Resource context, Selection sel, Property prop, ObjectValue target, List<Base> values, boolean extensionMode, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 470 boolean il = false; 471 Argument arg = null; 472 ExpressionNode expression = null; 473 if (sel.getField().hasDirective("slice")) { 474 Directive dir = sel.getField().directive("slice"); 475 String s = ((StringValue) dir.getArguments().get(0).getValues().get(0)).getValue(); 476 if (s.equals("$index")) 477 expression = magicExpression; 478 else 479 expression = fpe.parse(s); 480 } 481 if (sel.getField().hasDirective("flatten")) // special: instruction to drop this node... 482 il = prop.isList() && !sel.getField().hasDirective("first"); 483 else if (sel.getField().hasDirective("first")) { 484 if (expression != null) 485 throw new FHIRException("You cannot mix @slice and @first"); 486 arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), inheritedList)); 487 } else if (expression == null) 488 arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), prop.isList() || inheritedList)); 489 490 491 int index = 0; 492 for (Base value : values) { 493 String ss = ""; 494 if (expression != null) { 495 if (expression == magicExpression) 496 ss = suffix+'.'+Integer.toString(index); 497 else 498 ss = suffix+'.'+fpe.evaluateToString(null, null, null, value, expression); 499 if (!sel.getField().hasDirective("flatten")) 500 arg = target.addField(sel.getField().getAlias()+suffix, listStatus(sel.getField(), prop.isList() || inheritedList)); 501 } 502 503 if (value.isPrimitive() && !extensionMode) { 504 if (!sel.getField().getSelectionSet().isEmpty()) 505 throw new EGraphQLException("Encountered a selection set on a scalar field type"); 506 processPrimitive(arg, value); 507 } else { 508 if (sel.getField().getSelectionSet().isEmpty()) 509 throw new EGraphQLException("No Fields selected on a complex object"); 510 if (arg == null) 511 processObject(context, value, target, sel.getField().getSelectionSet(), il, ss); 512 else { 513 ObjectValue n = new ObjectValue(); 514 arg.addValue(n); 515 processObject(context, value, n, sel.getField().getSelectionSet(), il, ss); 516 } 517 } 518 if (sel.getField().hasDirective("first")) 519 return; 520 index++; 521 } 522 } 523 524 private void processVariables(Operation op) throws EGraphQLException { 525 for (Variable varRef : op.getVariables()) { 526 Argument varDef = null; 527 for (Argument v : graphQL.getVariables()) 528 if (v.getName().equals(varRef.getName())) 529 varDef = v; 530 if (varDef != null) 531 workingVariables.put(varRef.getName(), varDef); // todo: check type? 532 else if (varRef.getDefaultValue() != null) 533 workingVariables.put(varRef.getName(), new Argument(varRef.getName(), varRef.getDefaultValue())); 534 else 535 throw new EGraphQLException("No value found for variable "); 536 } 537 } 538 539 private boolean isPrimitive(String typename) { 540 return Utilities.existsInList(typename, "boolean", "integer", "string", "decimal", "uri", "base64Binary", "instant", "date", "dateTime", "time", "code", "oid", "id", "markdown", "unsignedInt", "positiveInt", "url", "canonical"); 541 } 542 543 private boolean isResourceName(String name, String suffix) { 544 if (!name.endsWith(suffix)) 545 return false; 546 name = name.substring(0, name.length()-suffix.length()); 547 return context.getResourceNamesAsSet().contains(name); 548 } 549 550 private void processObject(Resource context, Base source, ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 551 for (Selection sel : selection) { 552 if (sel.getField() != null) { 553 if (checkDirectives(sel.getField().getDirectives())) { 554 Property prop = source.getNamedProperty(sel.getField().getName()); 555 if ((prop == null) && sel.getField().getName().startsWith("_")) 556 prop = source.getNamedProperty(sel.getField().getName().substring(1)); 557 if (prop == null) { 558 if ((sel.getField().getName().equals("resourceType") && source instanceof Resource)) 559 target.addField("resourceType", listStatus(sel.getField(), false)).addValue(new StringValue(source.fhirType())); 560 else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("Reference"))) 561 processReference(context, source, sel.getField(), target, inheritedList, suffix); 562 else if ((sel.getField().getName().equals("resource") && source.fhirType().equals("canonical"))) 563 processCanonicalReference(context, source, sel.getField(), target, inheritedList, suffix); 564 else if (isResourceName(sel.getField().getName(), "List") && (source instanceof Resource)) 565 processReverseReferenceList((Resource) source, sel.getField(), target, inheritedList, suffix); 566 else if (isResourceName(sel.getField().getName(), "Connection") && (source instanceof Resource)) 567 processReverseReferenceSearch((Resource) source, sel.getField(), target, inheritedList, suffix); 568 else 569 throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType()); 570 } else { 571 if (!isPrimitive(prop.getTypeCode()) && sel.getField().getName().startsWith("_")) 572 throw new EGraphQLException("Unknown property "+sel.getField().getName()+" on "+source.fhirType()); 573 574 List<Base> vl = filter(context, prop, sel.getField().getName(), sel.getField().getArguments(), prop.getValues(), sel.getField().getName().startsWith("_")); 575 if (!vl.isEmpty()) 576 processValues(context, sel, prop, target, vl, sel.getField().getName().startsWith("_"), inheritedList, suffix); 577 } 578 } 579 } else if (sel.getInlineFragment() != null) { 580 if (checkDirectives(sel.getInlineFragment().getDirectives())) { 581 if (Utilities.noString(sel.getInlineFragment().getTypeCondition())) 582 throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid? 583 if (source.fhirType().equals(sel.getInlineFragment().getTypeCondition())) 584 processObject(context, source, target, sel.getInlineFragment().getSelectionSet(), inheritedList, suffix); 585 } 586 } else if (checkDirectives(sel.getFragmentSpread().getDirectives())) { 587 Fragment fragment = graphQL.getDocument().fragment(sel.getFragmentSpread().getName()); 588 if (fragment == null) 589 throw new EGraphQLException("Unable to resolve fragment "+sel.getFragmentSpread().getName()); 590 591 if (Utilities.noString(fragment.getTypeCondition())) 592 throw new EGraphQLException("Not done yet - inline fragment with no type condition"); // cause why? why instanceof it even valid? 593 if (source.fhirType().equals(fragment.getTypeCondition())) 594 processObject(context, source, target, fragment.getSelectionSet(), inheritedList, suffix); 595 } 596 } 597 } 598 599 private void processPrimitive(Argument arg, Base value) { 600 String s = value.fhirType(); 601 if (s.equals("integer") || s.equals("decimal") || s.equals("unsignedInt") || s.equals("positiveInt")) 602 arg.addValue(new NumberValue(value.primitiveValue())); 603 else if (s.equals("boolean")) 604 arg.addValue(new NameValue(value.primitiveValue())); 605 else 606 arg.addValue(new StringValue(value.primitiveValue())); 607 } 608 609 private void processReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 610 if (!(source instanceof Reference)) 611 throw new EGraphQLException("Not done yet"); 612 if (services == null) 613 throw new EGraphQLException("Resource Referencing services not provided"); 614 615 Reference ref = (Reference) source; 616 ReferenceResolution res = services.lookup(appInfo, context, ref); 617 if (res != null) { 618 if (targetTypeOk(field.getArguments(), res.getTarget())) { 619 Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList)); 620 ObjectValue obj = new ObjectValue(); 621 arg.addValue(obj); 622 processObject((Resource)res.getTargetContext(), (Base) res.getTarget(), obj, field.getSelectionSet(), inheritedList, suffix); 623 } 624 } 625 else if (!hasArgument(field.getArguments(), "optional", "true")) 626 throw new EGraphQLException("Unable to resolve reference to "+ref.getReference()); 627 } 628 629 private void processCanonicalReference(Resource context, Base source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 630 if (!(source instanceof CanonicalType)) 631 throw new EGraphQLException("Not done yet"); 632 if (services == null) 633 throw new EGraphQLException("Resource Referencing services not provided"); 634 635 Reference ref = new Reference(source.primitiveValue()); 636 ReferenceResolution res = services.lookup(appInfo, context, ref); 637 if (res != null) { 638 if (targetTypeOk(field.getArguments(), res.getTarget())) { 639 Argument arg = target.addField(field.getAlias() + suffix, listStatus(field, inheritedList)); 640 ObjectValue obj = new ObjectValue(); 641 arg.addValue(obj); 642 processObject((Resource)res.getTargetContext(), (Base) res.getTarget(), obj, field.getSelectionSet(), inheritedList, suffix); 643 } 644 } 645 else if (!hasArgument(field.getArguments(), "optional", "true")) 646 throw new EGraphQLException("Unable to resolve reference to "+ref.getReference()); 647 } 648 649 private ArgumentListStatus listStatus(Field field, boolean isList) { 650 if (field.hasDirective("singleton")) 651 return ArgumentListStatus.SINGLETON; 652 else if (isList) 653 return ArgumentListStatus.REPEATING; 654 else 655 return ArgumentListStatus.NOT_SPECIFIED; 656 } 657 658 private void processReverseReferenceList(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 659 if (services == null) 660 throw new EGraphQLException("Resource Referencing services not provided"); 661 List<IBaseResource> list = new ArrayList<>(); 662 List<Argument> params = new ArrayList<>(); 663 Argument parg = null; 664 for (Argument a : field.getArguments()) 665 if (!(a.getName().equals("_reference"))) 666 params.add(a); 667 else if ((parg == null)) 668 parg = a; 669 else 670 throw new EGraphQLException("Duplicate parameter _reference"); 671 if (parg == null) 672 throw new EGraphQLException("Missing parameter _reference"); 673 Argument arg = new Argument(); 674 params.add(arg); 675 arg.setName(getSingleValue(parg)); 676 arg.addValue(new StringValue(source.fhirType()+"/"+source.getId())); 677 services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), params, list); 678 arg = null; 679 ObjectValue obj = null; 680 681 List<Resource> vl = filterResources(field.argument("fhirpath"), list); 682 if (!vl.isEmpty()) { 683 arg = target.addField(field.getAlias()+suffix, listStatus(field, true)); 684 for (Resource v : vl) { 685 obj = new ObjectValue(); 686 arg.addValue(obj); 687 processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix); 688 } 689 } 690 } 691 692 private void processReverseReferenceSearch(Resource source, Field field, ObjectValue target, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 693 if (services == null) 694 throw new EGraphQLException("Resource Referencing services not provided"); 695 List<Argument> params = new ArrayList<Argument>(); 696 Argument parg = null; 697 for (Argument a : field.getArguments()) 698 if (!(a.getName().equals("_reference"))) 699 params.add(a); 700 else if ((parg == null)) 701 parg = a; 702 else 703 throw new EGraphQLException("Duplicate parameter _reference"); 704 if (parg == null) 705 throw new EGraphQLException("Missing parameter _reference"); 706 Argument arg = new Argument(); 707 params.add(arg); 708 arg.setName(getSingleValue(parg)); 709 arg.addValue(new StringValue(source.fhirType()+"/"+source.getId())); 710 Bundle bnd = (Bundle) services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params); 711 Base bndWrapper = new SearchWrapper(field.getName(), bnd); 712 arg = target.addField(field.getAlias()+suffix, listStatus(field, false)); 713 ObjectValue obj = new ObjectValue(); 714 arg.addValue(obj); 715 processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix); 716 } 717 718 private void processSearch(ObjectValue target, List<Selection> selection, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 719 for (Selection sel : selection) { 720 if ((sel.getField() == null)) 721 throw new EGraphQLException("Only field selections are allowed in this context"); 722 checkNoDirectives(sel.getField().getDirectives()); 723 724 if ((isResourceName(sel.getField().getName(), ""))) 725 processSearchSingle(target, sel.getField(), inheritedList, suffix); 726 else if ((isResourceName(sel.getField().getName(), "List"))) 727 processSearchSimple(target, sel.getField(), inheritedList, suffix); 728 else if ((isResourceName(sel.getField().getName(), "Connection"))) 729 processSearchFull(target, sel.getField(), inheritedList, suffix); 730 } 731 } 732 733 private void processSearchSingle(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 734 if (services == null) 735 throw new EGraphQLException("Resource Referencing services not provided"); 736 String id = ""; 737 for (Argument arg : field.getArguments()) 738 if ((arg.getName().equals("id"))) 739 id = getSingleValue(arg); 740 else 741 throw new EGraphQLException("Unknown/invalid parameter "+arg.getName()); 742 if (Utilities.noString(id)) 743 throw new EGraphQLException("No id found"); 744 Resource res = (Resource) services.lookup(appInfo, field.getName(), id); 745 if (res == null) 746 throw new EGraphQLException("Resource "+field.getName()+"/"+id+" not found"); 747 Argument arg = target.addField(field.getAlias()+suffix, listStatus(field, false)); 748 ObjectValue obj = new ObjectValue(); 749 arg.addValue(obj); 750 processObject(res, res, obj, field.getSelectionSet(), inheritedList, suffix); 751 } 752 753 private void processSearchSimple(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 754 if (services == null) 755 throw new EGraphQLException("Resource Referencing services not provided"); 756 List<IBaseResource> list = new ArrayList<>(); 757 services.listResources(appInfo, field.getName().substring(0, field.getName().length() - 4), field.getArguments(), list); 758 Argument arg = null; 759 ObjectValue obj = null; 760 761 List<Resource> vl = filterResources(field.argument("fhirpath"), list); 762 if (!vl.isEmpty()) { 763 arg = target.addField(field.getAlias()+suffix, listStatus(field, true)); 764 for (Resource v : vl) { 765 obj = new ObjectValue(); 766 arg.addValue(obj); 767 processObject(v, v, obj, field.getSelectionSet(), inheritedList, suffix); 768 } 769 } 770 } 771 772 private void processSearchFull(ObjectValue target, Field field, boolean inheritedList, String suffix) throws EGraphQLException, FHIRException { 773 if (services == null) 774 throw new EGraphQLException("Resource Referencing services not provided"); 775 List<Argument> params = new ArrayList<Argument>(); 776 Argument carg = null; 777 for ( Argument arg : field.getArguments()) 778 if (arg.getName().equals("cursor")) 779 carg = arg; 780 else 781 params.add(arg); 782 if ((carg != null)) { 783 params.clear();; 784 String[] parts = getSingleValue(carg).split(":"); 785 params.add(new Argument("search-id", new StringValue(parts[0]))); 786 params.add(new Argument("search-offset", new StringValue(parts[1]))); 787 } 788 789 Bundle bnd = (Bundle) services.search(appInfo, field.getName().substring(0, field.getName().length()-10), params); 790 SearchWrapper bndWrapper = new SearchWrapper(field.getName(), bnd); 791 Argument arg = target.addField(field.getAlias()+suffix, listStatus(field, false)); 792 ObjectValue obj = new ObjectValue(); 793 arg.addValue(obj); 794 processObject(null, bndWrapper, obj, field.getSelectionSet(), inheritedList, suffix); 795 } 796 797 private String getSingleValue(Argument arg) throws EGraphQLException { 798 List<Value> vl = resolveValues(arg, 1); 799 if (vl.size() == 0) 800 return ""; 801 return vl.get(0).toString(); 802 } 803 804 private List<Value> resolveValues(Argument arg) throws EGraphQLException { 805 return resolveValues(arg, -1, ""); 806 } 807 808 private List<Value> resolveValues(Argument arg, int max) throws EGraphQLException { 809 return resolveValues(arg, max, ""); 810 } 811 812 private List<Value> resolveValues(Argument arg, int max, String vars) throws EGraphQLException { 813 List<Value> result = new ArrayList<Value>(); 814 for (Value v : arg.getValues()) { 815 if (! (v instanceof VariableValue)) 816 result.add(v); 817 else { 818 if (vars.contains(":"+v.toString()+":")) 819 throw new EGraphQLException("Recursive reference to variable "+v.toString()); 820 Argument a = workingVariables.get(v.toString()); 821 if (a == null) 822 throw new EGraphQLException("No value found for variable \""+v.toString()+"\" in \""+arg.getName()+"\""); 823 List<Value> vl = resolveValues(a, -1, vars+":"+v.toString()+":"); 824 result.addAll(vl); 825 } 826 } 827 if ((max != -1 && result.size() > max)) 828 throw new EGraphQLException("Only "+Integer.toString(max)+" values are allowed for \""+arg.getName()+"\", but "+Integer.toString(result.size())+" enoucntered"); 829 return result; 830 } 831 832 833 834 835 public Object getAppInfo() { 836 return appInfo; 837 } 838 839 @Override 840 public void setAppInfo(Object appInfo) { 841 this.appInfo = appInfo; 842 } 843 844 public Resource getFocus() { 845 return focus; 846 } 847 848 @Override 849 public void setFocus(IBaseResource focus) { 850 this.focus = (Resource) focus; 851 } 852 853 public Package getGraphQL() { 854 return graphQL; 855 } 856 857 @Override 858 public void setGraphQL(Package graphQL) { 859 this.graphQL = graphQL; 860 } 861 862 @Override 863 public GraphQLResponse getOutput() { 864 return output; 865 } 866 867 public IGraphQLStorageServices getServices() { 868 return services; 869 } 870 871 @Override 872 public void setServices(IGraphQLStorageServices services) { 873 this.services = services; 874 } 875 876 877 // 878//{ GraphQLSearchWrapper } 879// 880//constructor GraphQLSearchWrapper.Create(bundle : Bundle); 881//var 882// s : String; 883//{ 884// inherited Create; 885// FBundle = bundle; 886// s = bundle_List.Matches["self"]; 887// FParseMap = TParseMap.create(s.Substring(s.IndexOf("?")+1)); 888//} 889// 890//destructor GraphQLSearchWrapper.Destroy; 891//{ 892// FParseMap.free; 893// FBundle.Free; 894// inherited; 895//} 896// 897//function GraphQLSearchWrapper.extractLink(name: String): String; 898//var 899// s : String; 900// pm : TParseMap; 901//{ 902// s = FBundle_List.Matches[name]; 903// if (s == "") 904// result = null 905// else 906// { 907// pm = TParseMap.create(s.Substring(s.IndexOf("?")+1)); 908// try 909// result = String.Create(pm.GetVar("search-id")+":"+pm.GetVar("search-offset")); 910// finally 911// pm.Free; 912// } 913// } 914//} 915// 916//function GraphQLSearchWrapper.extractParam(name: String; int : boolean): Base; 917//var 918// s : String; 919//{ 920// s = FParseMap.GetVar(name); 921// if (s == "") 922// result = null 923// else if (int) 924// result = Integer.Create(s) 925// else 926// result = String.Create(s); 927//} 928// 929//function GraphQLSearchWrapper.fhirType(): String; 930//{ 931// result = "*Connection"; 932//} 933// 934// // 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 935// 936//function GraphQLSearchWrapper.getPropertyValue(propName: string): Property; 937//var 938// list : List<GraphQLSearchEdge>; 939// be : BundleEntry; 940//{ 941// if (propName == "first") 942// result = Property.Create(self, propname, "string", false, String, extractLink("first")) 943// else if (propName == "previous") 944// result = Property.Create(self, propname, "string", false, String, extractLink("previous")) 945// else if (propName == "next") 946// result = Property.Create(self, propname, "string", false, String, extractLink("next")) 947// else if (propName == "last") 948// result = Property.Create(self, propname, "string", false, String, extractLink("last")) 949// else if (propName == "count") 950// result = Property.Create(self, propname, "integer", false, String, FBundle.totalElement) 951// else if (propName == "offset") 952// result = Property.Create(self, propname, "integer", false, Integer, extractParam("search-offset", true)) 953// else if (propName == "pagesize") 954// result = Property.Create(self, propname, "integer", false, Integer, extractParam("_count", true)) 955// else if (propName == "edges") 956// { 957// list = ArrayList<GraphQLSearchEdge>(); 958// try 959// for be in FBundle.getEntry() do 960// list.add(GraphQLSearchEdge.create(be)); 961// result = Property.Create(self, propname, "integer", true, Integer, List<Base>(list)); 962// finally 963// list.Free; 964// } 965// } 966// else 967// result = null; 968//} 969// 970//private void GraphQLSearchWrapper.SetBundle(const Value: Bundle); 971//{ 972// FBundle.Free; 973// FBundle = Value; 974//} 975// 976//{ GraphQLSearchEdge } 977// 978//constructor GraphQLSearchEdge.Create(entry: BundleEntry); 979//{ 980// inherited Create; 981// FEntry = entry; 982//} 983// 984//destructor GraphQLSearchEdge.Destroy; 985//{ 986// FEntry.Free; 987// inherited; 988//} 989// 990//function GraphQLSearchEdge.fhirType(): String; 991//{ 992// result = "*Edge"; 993//} 994// 995//function GraphQLSearchEdge.getPropertyValue(propName: string): Property; 996//{ 997// if (propName == "mode") 998// { 999// if (FEntry.search != null) 1000// result = Property.Create(self, propname, "code", false, Enum, FEntry.search.modeElement) 1001// else 1002// result = Property.Create(self, propname, "code", false, Enum, Base(null)); 1003// } 1004// else if (propName == "score") 1005// { 1006// if (FEntry.search != null) 1007// result = Property.Create(self, propname, "decimal", false, Decimal, FEntry.search.scoreElement) 1008// else 1009// result = Property.Create(self, propname, "decimal", false, Decimal, Base(null)); 1010// } 1011// else if (propName == "resource") 1012// result = Property.Create(self, propname, "resource", false, Resource, FEntry.getResource()) 1013// else 1014// result = null; 1015//} 1016// 1017//private void GraphQLSearchEdge.SetEntry(const Value: BundleEntry); 1018//{ 1019// FEntry.Free; 1020// FEntry = value; 1021//} 1022// 1023}