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