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