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