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