From b2ceb87a70afb62d8ed012d30e00bb1c4b8e8bbd Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sat, 3 Mar 2018 17:50:29 -0500 Subject: [PATCH] Work on #873 --- .../hl7/fhir/dstu3/utils/FHIRPathEngine.java | 442 +++++---- .../org/hl7/fhir/r4/utils/FHIRPathEngine.java | 119 ++- .../FhirInstanceValidatorDstu3Test.java | 31 + .../validation/FhirInstanceValidatorTest.java | 61 +- .../FhirInstanceValidatorR4Test.java | 939 +++++++++--------- 5 files changed, 868 insertions(+), 724 deletions(-) diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/FHIRPathEngine.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/FHIRPathEngine.java index 5dadf69c830..90e00649c7d 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/FHIRPathEngine.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/utils/FHIRPathEngine.java @@ -20,6 +20,8 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.ElementUtil; +import static org.apache.commons.lang3.StringUtils.length; + /** * * @author Grahame Grieve @@ -33,7 +35,7 @@ public class FHIRPathEngine { private Map allTypes = new HashMap(); // if the fhir path expressions are allowed to use constants beyond those defined in the specification - // the application can implement them by providing a constant resolver + // the application can implement them by providing a constant resolver public interface IEvaluationContext { public class FunctionDetails { private String description; @@ -60,19 +62,19 @@ public class FHIRPathEngine { /** * A constant reference - e.g. a reference to a name that must be resolved in context. * The % will be removed from the constant name before this is invoked. - * + * * This will also be called if the host invokes the FluentPath engine with a context of null - * + * * @param appContext - content passed into the fluent path engine * @param name - name reference to resolve * @return the value of the reference (or null, if it's not valid, though can throw an exception if desired) */ public Base resolveConstant(Object appContext, String name) throws PathEngineException; public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException; - + /** * when the .log() function is called - * + * * @param argument * @param focus * @return @@ -81,12 +83,12 @@ public class FHIRPathEngine { // extensibility for functions /** - * + * * @param functionName * @return null if the function is not known */ public FunctionDetails resolveFunction(String functionName); - + /** * Check the function parameters, and throw an error if they are incorrect, or return the type for the function * @param functionName @@ -94,7 +96,7 @@ public class FHIRPathEngine { * @return */ public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException; - + /** * @param appContext * @param functionName @@ -102,7 +104,7 @@ public class FHIRPathEngine { * @return */ public List executeFunction(Object appContext, String functionName, List> parameters); - + /** * Implementation of resolve() function. Passed a string, return matching resource, if one is known - else null * @param appInfo @@ -145,17 +147,17 @@ public class FHIRPathEngine { /** * Given an item, return all the children that conform to the pattern described in name - * + * * Possible patterns: * - a simple name (which may be the base of a name with [] e.g. value[x]) * - a name with a type replacement e.g. valueCodeableConcept * - * which means all children * - ** which means all descendants - * + * * @param item * @param name * @param result - * @throws FHIRException + * @throws FHIRException */ protected void getChildrenByName(Base item, String name, List result) throws FHIRException { Base[] list = item.listChildrenByName(name, false); @@ -168,10 +170,10 @@ public class FHIRPathEngine { // --- public API ------------------------------------------------------- /** * Parse a path for later use using execute - * + * * @param path * @return - * @throws PathEngineException + * @throws PathEngineException * @throws Exception */ public ExpressionNode parse(String path) throws FHIRLexerException { @@ -182,39 +184,39 @@ public class FHIRPathEngine { if (!lexer.done()) throw lexer.error("Premature ExpressionNode termination at unexpected token \""+lexer.getCurrent()+"\""); result.check(); - return result; + return result; } /** * Parse a path that is part of some other syntax - * + * * @param path * @return - * @throws PathEngineException + * @throws PathEngineException * @throws Exception */ public ExpressionNode parse(FHIRLexer lexer) throws FHIRLexerException { ExpressionNode result = parseExpression(lexer, true); result.check(); - return result; + return result; } /** * check that paths referred to in the ExpressionNode are valid - * + * * xPathStartsWithValueRef is a hack work around for the fact that FHIR Path sometimes needs a different starting point than the xpath - * + * * returns a list of the possible types that might be returned by executing the ExpressionNode against a particular context - * + * * @param context - the logical type against which this path is applied * @param path - the FHIR Path statement to check - * @throws DefinitionException - * @throws PathEngineException + * @throws DefinitionException + * @throws PathEngineException * @if the path is not valid */ public TypeDetails check(Object appContext, String resourceType, String context, ExpressionNode expr) throws FHIRLexerException, PathEngineException, DefinitionException { - // if context is a path that refers to a type, do that conversion now - TypeDetails types; + // if context is a path that refers to a type, do that conversion now + TypeDetails types; if (context == null) { types = null; // this is a special case; the first path reference will have to resolve to something in the context } else if (!context.contains(".")) { @@ -226,18 +228,18 @@ public class FHIRPathEngine { ctxt = resourceType.substring(0, resourceType.lastIndexOf("/")+1)+ctxt; } StructureDefinition sd = worker.fetchResource(StructureDefinition.class, ctxt); - if (sd == null) + if (sd == null) throw new PathEngineException("Unknown context "+context); ElementDefinitionMatch ed = getElementDefinition(sd, context, true); - if (ed == null) + if (ed == null) throw new PathEngineException("Unknown context element "+context); - if (ed.fixedType != null) + if (ed.fixedType != null) types = new TypeDetails(CollectionStatus.SINGLETON, ed.fixedType); - else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) + else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) types = new TypeDetails(CollectionStatus.SINGLETON, ctxt+"#"+context); else { types = new TypeDetails(CollectionStatus.SINGLETON); - for (TypeRefComponent t : ed.getDefinition().getType()) + for (TypeRefComponent t : ed.getDefinition().getType()) types.addType(t.getCode()); } } @@ -246,21 +248,21 @@ public class FHIRPathEngine { } public TypeDetails check(Object appContext, StructureDefinition sd, String context, ExpressionNode expr) throws FHIRLexerException, PathEngineException, DefinitionException { - // if context is a path that refers to a type, do that conversion now - TypeDetails types; + // if context is a path that refers to a type, do that conversion now + TypeDetails types; if (!context.contains(".")) { types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl()); } else { ElementDefinitionMatch ed = getElementDefinition(sd, context, true); - if (ed == null) + if (ed == null) throw new PathEngineException("Unknown context element "+context); - if (ed.fixedType != null) + if (ed.fixedType != null) types = new TypeDetails(CollectionStatus.SINGLETON, ed.fixedType); - else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) + else if (ed.getDefinition().getType().isEmpty() || isAbstractType(ed.getDefinition().getType())) types = new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl()+"#"+context); else { types = new TypeDetails(CollectionStatus.SINGLETON); - for (TypeRefComponent t : ed.getDefinition().getType()) + for (TypeRefComponent t : ed.getDefinition().getType()) types.addType(t.getCode()); } } @@ -269,7 +271,7 @@ public class FHIRPathEngine { } public TypeDetails check(Object appContext, StructureDefinition sd, ExpressionNode expr) throws FHIRLexerException, PathEngineException, DefinitionException { - // if context is a path that refers to a type, do that conversion now + // if context is a path that refers to a type, do that conversion now TypeDetails types = null; // this is a special case; the first path reference will have to resolve to something in the context return executeType(new ExecutionTypeContext(appContext, sd == null ? null : sd.getUrl(), null, types), types, expr, true); } @@ -281,11 +283,11 @@ public class FHIRPathEngine { /** * evaluate a path and return the matching elements - * + * * @param base - the object against which the path is being evaluated * @param ExpressionNode - the parsed ExpressionNode statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public List evaluate(Base base, ExpressionNode ExpressionNode) throws FHIRException { @@ -298,11 +300,11 @@ public class FHIRPathEngine { /** * evaluate a path and return the matching elements - * + * * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public List evaluate(Base base, String path) throws FHIRException { @@ -316,11 +318,11 @@ public class FHIRPathEngine { /** * evaluate a path and return the matching elements - * + * * @param base - the object against which the path is being evaluated * @param ExpressionNode - the parsed ExpressionNode statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public List evaluate(Object appContext, Resource resource, Base base, ExpressionNode ExpressionNode) throws FHIRException { @@ -333,11 +335,11 @@ public class FHIRPathEngine { /** * evaluate a path and return the matching elements - * + * * @param base - the object against which the path is being evaluated * @param ExpressionNode - the parsed ExpressionNode statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public List evaluate(Object appContext, Base resource, Base base, ExpressionNode ExpressionNode) throws FHIRException { @@ -350,11 +352,11 @@ public class FHIRPathEngine { /** * evaluate a path and return the matching elements - * + * * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public List evaluate(Object appContext, Resource resource, Base base, String path) throws FHIRException { @@ -368,11 +370,11 @@ public class FHIRPathEngine { /** * evaluate a path and return true or false (e.g. for an invariant) - * + * * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public boolean evaluateToBoolean(Resource resource, Base base, String path) throws FHIRException { @@ -381,11 +383,11 @@ public class FHIRPathEngine { /** * evaluate a path and return true or false (e.g. for an invariant) - * + * * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public boolean evaluateToBoolean(Resource resource, Base base, ExpressionNode node) throws FHIRException { @@ -394,12 +396,12 @@ public class FHIRPathEngine { /** * evaluate a path and return true or false (e.g. for an invariant) - * + * * @param appinfo - application context * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public boolean evaluateToBoolean(Object appInfo, Resource resource, Base base, ExpressionNode node) throws FHIRException { @@ -408,11 +410,11 @@ public class FHIRPathEngine { /** * evaluate a path and return true or false (e.g. for an invariant) - * + * * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public boolean evaluateToBoolean(Base resource, Base base, ExpressionNode node) throws FHIRException { @@ -421,11 +423,11 @@ public class FHIRPathEngine { /** * evaluate a path and a string containing the outcome (for display) - * + * * @param base - the object against which the path is being evaluated * @param path - the FHIR Path statement to use * @return - * @throws FHIRException + * @throws FHIRException * @ */ public String evaluateToString(Base base, String path) throws FHIRException { @@ -438,7 +440,7 @@ public class FHIRPathEngine { /** * worker routine for converting a set of objects to a string representation - * + * * @param items - result from @evaluate * @return */ @@ -446,7 +448,7 @@ public class FHIRPathEngine { StringBuilder b = new StringBuilder(); boolean first = true; for (Base item : items) { - if (first) + if (first) first = false; else b.append(','); @@ -459,13 +461,13 @@ public class FHIRPathEngine { private String convertToString(Base item) { if (item.isPrimitive()) return item.primitiveValue(); - else + else return item.toString(); } /** * worker routine for converting a set of objects to a boolean representation (for invariants) - * + * * @param items - result from @evaluate * @return */ @@ -474,7 +476,7 @@ public class FHIRPathEngine { return false; else if (items.size() == 1 && items.get(0) instanceof BooleanType) return ((BooleanType) items.get(0)).getValue(); - else + else return items.size() > 0; } @@ -509,11 +511,11 @@ public class FHIRPathEngine { private Base context; private Base thisItem; private Map aliases; - + public ExecutionContext(Object appInfo, Base resource, Base context, Map aliases, Base thisItem) { this.appInfo = appInfo; this.context = context; - this.resource = resource; + this.resource = resource; this.aliases = aliases; this.thisItem = thisItem; } @@ -527,10 +529,10 @@ public class FHIRPathEngine { if (aliases == null) aliases = new HashMap(); else - aliases = new HashMap(aliases); // clone it, since it's going to change + aliases = new HashMap(aliases); // clone it, since it's going to change if (focus.size() > 1) throw new FHIRException("Attempt to alias a collection, not a singleton"); - aliases.put(name, focus.size() == 0 ? null : focus.get(0)); + aliases.put(name, focus.size() == 0 ? null : focus.get(0)); } public Base getAlias(String name) { return aliases == null ? null : aliases.get(name); @@ -538,7 +540,7 @@ public class FHIRPathEngine { } private class ExecutionTypeContext { - private Object appInfo; + private Object appInfo; private String resource; private String context; private TypeDetails thisItem; @@ -550,7 +552,7 @@ public class FHIRPathEngine { this.resource = resource; this.context = context; this.thisItem = thisItem; - + } public String getResource() { return resource; @@ -582,12 +584,12 @@ public class FHIRPathEngine { lexer.next(); result.setKind(Kind.Group); result.setGroup(parseExpression(lexer, true)); - if (!")".equals(lexer.getCurrent())) + if (!")".equals(lexer.getCurrent())) throw lexer.error("Found "+lexer.getCurrent()+" expecting a \")\""); result.setEnd(lexer.getCurrentLocation()); lexer.next(); } else { - if (!lexer.isToken() && !lexer.getCurrent().startsWith("\"")) + if (!lexer.isToken() && !lexer.getCurrent().startsWith("\"")) throw lexer.error("Found "+lexer.getCurrent()+" expecting a token name"); if (lexer.getCurrent().startsWith("\"")) result.setName(lexer.readConstant("Path Name")); @@ -609,7 +611,7 @@ public class FHIRPathEngine { result.setKind(Kind.Function); result.setFunction(f); lexer.next(); - while (!")".equals(lexer.getCurrent())) { + while (!")".equals(lexer.getCurrent())) { result.getParameters().add(parseExpression(lexer, true)); if (",".equals(lexer.getCurrent())) lexer.next(); @@ -655,9 +657,9 @@ public class FHIRPathEngine { } private ExpressionNode organisePrecedence(FHIRLexer lexer, ExpressionNode node) { - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Times, Operation.DivideBy, Operation.Div, Operation.Mod)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Plus, Operation.Minus, Operation.Concatenate)); - node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Union)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Times, Operation.DivideBy, Operation.Div, Operation.Mod)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Plus, Operation.Minus, Operation.Concatenate)); + node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Union)); node = gatherPrecedence(lexer, node, EnumSet.of(Operation.LessThen, Operation.Greater, Operation.LessOrEqual, Operation.GreaterOrEqual)); node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Is)); node = gatherPrecedence(lexer, node, EnumSet.of(Operation.Equals, Operation.Equivalent, Operation.NotEquals, Operation.NotEquivalent)); @@ -686,7 +688,7 @@ public class FHIRPathEngine { work = work || ops.contains(focus.getOperation()); focus = focus.getOpNext(); } - } + } if (!work) return start; @@ -724,12 +726,12 @@ public class FHIRPathEngine { // now look for another sequence, and start it ExpressionNode node = group; focus = group.getOpNext(); - if (focus != null) { + if (focus != null) { while (focus != null && !ops.contains(focus.getOperation())) { node = focus; focus = focus.getOpNext(); } - if (focus != null) { // && (focus.Operation in Ops) - must be true + if (focus != null) { // && (focus.Operation in Ops) - must be true group = newGroup(lexer, focus); node.setOpNext(group); } @@ -756,14 +758,14 @@ public class FHIRPathEngine { char ch = s.charAt(i); if (ch == '\\') { switch (ch) { - case 't': + case 't': case 'r': - case 'n': - case 'f': + case 'n': + case 'f': case '\'': - case '\\': - case '/': - i++; + case '\\': + case '/': + i++; break; case 'u': if (!Utilities.isHex("0x"+s.substring(i, i+4))) @@ -860,7 +862,7 @@ public class FHIRPathEngine { for (Base base : outcome) if (base != null) work.add(base); - } + } break; case Function: List work2 = evaluateFunction(context, focus, exp); @@ -917,7 +919,7 @@ public class FHIRPathEngine { return isBoolean(left, true) ? makeBoolean(true) : null; case Implies: return convertToBoolean(left) ? null : makeBoolean(true); - default: + default: return null; } } @@ -1017,7 +1019,7 @@ public class FHIRPathEngine { } if (v.length() > 10) return new DateTimeType(value); - else + else return new DateType(value); } @@ -1058,25 +1060,25 @@ public class FHIRPathEngine { if (ch == '\\') { i++; switch (s.charAt(i)) { - case 't': + case 't': b.append('\t'); break; case 'r': b.append('\r'); break; - case 'n': + case 'n': b.append('\n'); break; - case 'f': + case 'f': b.append('\f'); break; case '\'': b.append('\''); break; - case '\\': + case '\\': b.append('\\'); break; - case '/': + case '/': b.append('/'); break; case 'u': @@ -1124,7 +1126,7 @@ public class FHIRPathEngine { case Mod: return opMod(left, right); case Is: return opIs(left, right); case As: return opAs(left, right); - default: + default: throw new Error("Not Done Yet: "+operation.toCode()); } } @@ -1144,7 +1146,7 @@ public class FHIRPathEngine { private List opIs(List left, List right) { List result = new ArrayList(); - if (left.size() != 1 || right.size() != 1) + if (left.size() != 1 || right.size() != 1) result.add(new BooleanType(false)); else { String tn = convertToString(right); @@ -1171,14 +1173,14 @@ public class FHIRPathEngine { case And: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); case Xor: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); case Implies : return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Times: + case Times: TypeDetails result = new TypeDetails(CollectionStatus.SINGLETON); if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) result.addType("integer"); else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) result.addType("decimal"); return result; - case DivideBy: + case DivideBy: result = new TypeDetails(CollectionStatus.SINGLETON); if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) result.addType("decimal"); @@ -1204,8 +1206,8 @@ public class FHIRPathEngine { else if (left.hasType(worker, "integer", "decimal") && right.hasType(worker, "integer", "decimal")) result.addType("decimal"); return result; - case Div: - case Mod: + case Div: + case Mod: result = new TypeDetails(CollectionStatus.SINGLETON); if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) result.addType("integer"); @@ -1214,7 +1216,7 @@ public class FHIRPathEngine { return result; case In: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); case Contains: return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - default: + default: return null; } } @@ -1226,7 +1228,7 @@ public class FHIRPathEngine { boolean res = true; for (int i = 0; i < left.size(); i++) { - if (!doEquals(left.get(i), right.get(i))) { + if (!doEquals(left.get(i), right.get(i))) { res = false; break; } @@ -1240,7 +1242,7 @@ public class FHIRPathEngine { boolean res = true; for (int i = 0; i < left.size(); i++) { - if (!doEquals(left.get(i), right.get(i))) { + if (!doEquals(left.get(i), right.get(i))) { res = false; break; } @@ -1263,7 +1265,7 @@ public class FHIRPathEngine { if (left.hasType("integer", "decimal", "unsignedInt", "positiveInt") && right.hasType("integer", "decimal", "unsignedInt", "positiveInt")) return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); if (left.hasType("date", "dateTime", "time", "instant") && right.hasType("date", "dateTime", "time", "instant")) - return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); + return compareDateTimeElements(left, right) == 0; if (left.hasType("string", "id", "code", "uri") && right.hasType("string", "id", "code", "uri")) return Utilities.equivalent(convertToString(left), convertToString(right)); @@ -1316,13 +1318,13 @@ public class FHIRPathEngine { if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { Base l = left.get(0); Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) + if (l.hasType("string") && r.hasType("string")) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); - else if ((l.hasType("integer") || l.hasType("decimal")) && (r.hasType("integer") || r.hasType("decimal"))) + else if ((l.hasType("integer") || l.hasType("decimal")) && (r.hasType("integer") || r.hasType("decimal"))) return makeBoolean(new Double(l.primitiveValue()) < new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); - else if ((l.hasType("time")) && (r.hasType("time"))) + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) < 0); + else if ((l.hasType("time")) && (r.hasType("time"))) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { List lUnit = left.get(0).listChildrenByName("unit"); @@ -1340,13 +1342,13 @@ public class FHIRPathEngine { if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { Base l = left.get(0); Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) + if (l.hasType("string") && r.hasType("string")) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); - else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) + else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) return makeBoolean(new Double(l.primitiveValue()) > new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); - else if ((l.hasType("time")) && (r.hasType("time"))) + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) > 0); + else if ((l.hasType("time")) && (r.hasType("time"))) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { List lUnit = left.get(0).listChildrenByName("unit"); @@ -1364,13 +1366,13 @@ public class FHIRPathEngine { if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { Base l = left.get(0); Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) + if (l.hasType("string") && r.hasType("string")) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); - else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) + else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) return makeBoolean(new Double(l.primitiveValue()) <= new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); - else if ((l.hasType("time")) && (r.hasType("time"))) + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) <= 0); + else if ((l.hasType("time")) && (r.hasType("time"))) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { List lUnits = left.get(0).listChildrenByName("unit"); @@ -1390,13 +1392,13 @@ public class FHIRPathEngine { if (left.size() == 1 && right.size() == 1 && left.get(0).isPrimitive() && right.get(0).isPrimitive()) { Base l = left.get(0); Base r = right.get(0); - if (l.hasType("string") && r.hasType("string")) + if (l.hasType("string") && r.hasType("string")) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); - else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) + else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) return makeBoolean(new Double(l.primitiveValue()) >= new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); - else if ((l.hasType("time")) && (r.hasType("time"))) + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) >= 0); + else if ((l.hasType("time")) && (r.hasType("time"))) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { List lUnit = left.get(0).listChildrenByName("unit"); @@ -1410,6 +1412,22 @@ public class FHIRPathEngine { return new ArrayList(); } + private int compareDateTimeElements(Base theL, Base theR) { + String dateLeftString = theL.primitiveValue(); + if (length(dateLeftString) > 10) { + DateTimeType dateLeft = new DateTimeType(dateLeftString); + dateLeft.setTimeZoneZulu(true); + dateLeftString = dateLeft.getValueAsString(); + } + String dateRightString = theR.primitiveValue(); + if (length(dateRightString) > 10) { + DateTimeType dateRight = new DateTimeType(dateRightString); + dateRight.setTimeZoneZulu(true); + dateRightString = dateRight.getValueAsString(); + } + return dateLeftString.compareTo(dateRightString); + } + private List opIn(List left, List right) { boolean ans = true; for (Base l : left) { @@ -1461,11 +1479,11 @@ public class FHIRPathEngine { List result = new ArrayList(); Base l = left.get(0); Base r = right.get(0); - if (l.hasType("string", "id", "code", "uri") && r.hasType("string", "id", "code", "uri")) + if (l.hasType("string", "id", "code", "uri") && r.hasType("string", "id", "code", "uri")) result.add(new StringType(l.primitiveValue() + r.primitiveValue())); - else if (l.hasType("integer") && r.hasType("integer")) + else if (l.hasType("integer") && r.hasType("integer")) result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) + Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) result.add(new DecimalType(new BigDecimal(l.primitiveValue()).add(new BigDecimal(r.primitiveValue())))); else throw new PathEngineException(String.format("Error performing +: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); @@ -1490,9 +1508,9 @@ public class FHIRPathEngine { Base l = left.get(0); Base r = right.get(0); - if (l.hasType("integer") && r.hasType("integer")) + if (l.hasType("integer") && r.hasType("integer")) result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) * Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) result.add(new DecimalType(new BigDecimal(l.primitiveValue()).multiply(new BigDecimal(r.primitiveValue())))); else throw new PathEngineException(String.format("Error performing *: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); @@ -1535,7 +1553,7 @@ public class FHIRPathEngine { return new ArrayList(); else if (convertToBoolean(left) && convertToBoolean(right)) return makeBoolean(true); - else + else return makeBoolean(false); } @@ -1550,22 +1568,22 @@ public class FHIRPathEngine { return makeBoolean(true); else if (left.isEmpty() || right.isEmpty()) return new ArrayList(); - else + else return makeBoolean(false); } private List opXor(List left, List right) { if (left.isEmpty() || right.isEmpty()) return new ArrayList(); - else + else return makeBoolean(convertToBoolean(left) ^ convertToBoolean(right)); } private List opImplies(List left, List right) { - if (!convertToBoolean(left)) + if (!convertToBoolean(left)) return makeBoolean(true); else if (right.size() == 0) - return new ArrayList(); + return new ArrayList(); else return makeBoolean(convertToBoolean(right)); } @@ -1589,9 +1607,9 @@ public class FHIRPathEngine { Base l = left.get(0); Base r = right.get(0); - if (l.hasType("integer") && r.hasType("integer")) + if (l.hasType("integer") && r.hasType("integer")) result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) - Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) result.add(new DecimalType(new BigDecimal(l.primitiveValue()).subtract(new BigDecimal(r.primitiveValue())))); else throw new PathEngineException(String.format("Error performing -: left and right operand have incompatible or illegal types (%s, %s)", left.get(0).fhirType(), right.get(0).fhirType())); @@ -1649,9 +1667,9 @@ public class FHIRPathEngine { Base l = left.get(0); Base r = right.get(0); - if (l.hasType("integer") && r.hasType("integer")) + if (l.hasType("integer") && r.hasType("integer")) result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) / Integer.parseInt(r.primitiveValue()))); - else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { + else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { Decimal d1; try { d1 = new Decimal(l.primitiveValue()); @@ -1684,7 +1702,7 @@ public class FHIRPathEngine { Base l = left.get(0); Base r = right.get(0); - if (l.hasType("integer") && r.hasType("integer")) + if (l.hasType("integer") && r.hasType("integer")) result.add(new IntegerType(Integer.parseInt(l.primitiveValue()) % Integer.parseInt(r.primitiveValue()))); else if (l.hasType("decimal", "integer") && r.hasType("decimal", "integer")) { Decimal d1; @@ -1703,9 +1721,9 @@ public class FHIRPathEngine { private TypeDetails readConstantType(ExecutionTypeContext context, String constant) throws PathEngineException { - if (constant.equals("true")) + if (constant.equals("true")) return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - else if (constant.equals("false")) + else if (constant.equals("false")) return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); else if (Utilities.isInteger(constant)) return new TypeDetails(CollectionStatus.SINGLETON, "integer"); @@ -1747,11 +1765,11 @@ public class FHIRPathEngine { } private List execute(ExecutionContext context, Base item, ExpressionNode exp, boolean atEntry) throws FHIRException { - List result = new ArrayList(); + List result = new ArrayList(); if (atEntry && Character.isUpperCase(exp.getName().charAt(0))) {// special case for start up - if (item.isResource() && item.fhirType().equals(exp.getName())) + if (item.isResource() && item.fhirType().equals(exp.getName())) result.add(item); - } else + } else getChildrenByName(item, exp.getName(), result); if (result.size() == 0 && atEntry && context.appInfo != null) { Base temp = hostServices.resolveConstant(context.appInfo, exp.getName()); @@ -1760,7 +1778,7 @@ public class FHIRPathEngine { } } return result; - } + } private TypeDetails executeContextType(ExecutionTypeContext context, String name) throws PathEngineException, DefinitionException { if (hostServices == null) @@ -1795,46 +1813,46 @@ public class FHIRPathEngine { paramTypes.add(executeType(context, focus, expr, true)); } switch (exp.getFunction()) { - case Empty : + case Empty : return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Not : + case Not : return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Exists : + case Exists : return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); case SubsetOf : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); } case SupersetOf : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, focus); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); } - case IsDistinct : + case IsDistinct : return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Distinct : + case Distinct : return focus; - case Count : + case Count : return new TypeDetails(CollectionStatus.SINGLETON, "integer"); - case Where : + case Where : return focus; - case Select : + case Select : return anything(focus.getCollectionStatus()); - case All : + case All : return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Repeat : + case Repeat : return anything(focus.getCollectionStatus()); case Item : { checkOrdered(focus, "item"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); - return focus; + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); + return focus; } case As : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); return new TypeDetails(CollectionStatus.SINGLETON, exp.getParameters().get(0).getName()); } case Is : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); } case Single : return focus.toSingleton(); @@ -1852,12 +1870,12 @@ public class FHIRPathEngine { } case Skip : { checkOrdered(focus, "skip"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); return focus; } case Take : { checkOrdered(focus, "take"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer")); return focus; } case Iif : { @@ -1881,76 +1899,76 @@ public class FHIRPathEngine { } case Substring : { checkContextString(focus, "subString"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer"), new TypeDetails(CollectionStatus.SINGLETON, "integer")); - return new TypeDetails(CollectionStatus.SINGLETON, "string"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "integer"), new TypeDetails(CollectionStatus.SINGLETON, "integer")); + return new TypeDetails(CollectionStatus.SINGLETON, "string"); } case StartsWith : { checkContextString(focus, "startsWith"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); } case EndsWith : { checkContextString(focus, "endsWith"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); } case Matches : { checkContextString(focus, "matches"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); } case ReplaceMatches : { checkContextString(focus, "replaceMatches"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "string"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "string"); } case Contains : { checkContextString(focus, "contains"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); } case Replace : { checkContextString(focus, "replace"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, "string")); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string"), new TypeDetails(CollectionStatus.SINGLETON, "string")); return new TypeDetails(CollectionStatus.SINGLETON, "string"); } - case Length : { + case Length : { checkContextPrimitive(focus, "length"); return new TypeDetails(CollectionStatus.SINGLETON, "integer"); } - case Children : + case Children : return childTypes(focus, "*"); - case Descendants : + case Descendants : return childTypes(focus, "**"); case MemberOf : { checkContextCoded(focus, "memberOf"); - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); } case Trace : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return focus; + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return focus; } - case Today : + case Today : return new TypeDetails(CollectionStatus.SINGLETON, "date"); - case Now : + case Now : return new TypeDetails(CollectionStatus.SINGLETON, "dateTime"); case Resolve : { checkContextReference(focus, "resolve"); - return new TypeDetails(CollectionStatus.SINGLETON, "DomainResource"); + return new TypeDetails(CollectionStatus.SINGLETON, "DomainResource"); } case Extension : { - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return new TypeDetails(CollectionStatus.SINGLETON, "Extension"); + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return new TypeDetails(CollectionStatus.SINGLETON, "Extension"); } - case HasValue : + case HasValue : return new TypeDetails(CollectionStatus.SINGLETON, "boolean"); - case Alias : - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return anything(CollectionStatus.SINGLETON); - case AliasAs : - checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); - return focus; + case Alias : + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return anything(CollectionStatus.SINGLETON); + case AliasAs : + checkParamTypes(exp.getFunction().toCode(), paramTypes, new TypeDetails(CollectionStatus.SINGLETON, "string")); + return focus; case Custom : { return hostServices.checkFunction(context.appInfo, exp.getName(), paramTypes); } @@ -1970,43 +1988,43 @@ public class FHIRPathEngine { i++; for (String a : actual.getTypes()) { if (!pt.hasType(worker, a)) - throw new PathEngineException("The parameter type '"+a+"' is not legal for "+funcName+" parameter "+Integer.toString(i)+". expecting "+pt.toString()); + throw new PathEngineException("The parameter type '"+a+"' is not legal for "+funcName+" parameter "+Integer.toString(i)+". expecting "+pt.toString()); } } } private void checkOrdered(TypeDetails focus, String name) throws PathEngineException { if (focus.getCollectionStatus() == CollectionStatus.UNORDERED) - throw new PathEngineException("The function '"+name+"'() can only be used on ordered collections"); + throw new PathEngineException("The function '"+name+"'() can only be used on ordered collections"); } private void checkContextReference(TypeDetails focus, String name) throws PathEngineException { if (!focus.hasType(worker, "string") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "Reference")) - throw new PathEngineException("The function '"+name+"'() can only be used on string, uri, Reference"); + throw new PathEngineException("The function '"+name+"'() can only be used on string, uri, Reference"); } private void checkContextCoded(TypeDetails focus, String name) throws PathEngineException { if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "Coding") && !focus.hasType(worker, "CodeableConcept")) - throw new PathEngineException("The function '"+name+"'() can only be used on string, code, uri, Coding, CodeableConcept"); + throw new PathEngineException("The function '"+name+"'() can only be used on string, code, uri, Coding, CodeableConcept"); } private void checkContextString(TypeDetails focus, String name) throws PathEngineException { if (!focus.hasType(worker, "string") && !focus.hasType(worker, "code") && !focus.hasType(worker, "uri") && !focus.hasType(worker, "id")) - throw new PathEngineException("The function '"+name+"'() can only be used on string, uri, code, id, but found "+focus.describe()); + throw new PathEngineException("The function '"+name+"'() can only be used on string, uri, code, id, but found "+focus.describe()); } private void checkContextPrimitive(TypeDetails focus, String name) throws PathEngineException { if (!focus.hasType(primitiveTypes)) - throw new PathEngineException("The function '"+name+"'() can only be used on "+primitiveTypes.toString()); + throw new PathEngineException("The function '"+name+"'() can only be used on "+primitiveTypes.toString()); } private TypeDetails childTypes(TypeDetails focus, String mask) throws PathEngineException, DefinitionException { TypeDetails result = new TypeDetails(CollectionStatus.UNORDERED); - for (String f : focus.getTypes()) + for (String f : focus.getTypes()) getChildTypesByName(f, mask, result); return result; } @@ -2065,9 +2083,9 @@ public class FHIRPathEngine { case HasValue : return funcHasValue(context, focus, exp); case AliasAs : return funcAliasAs(context, focus, exp); case Alias : return funcAlias(context, focus, exp); - case Custom: { + case Custom: { List> params = new ArrayList>(); - for (ExpressionNode p : exp.getParameters()) + for (ExpressionNode p : exp.getParameters()) params.add(execute(context, focus, p, true)); return hostServices.executeFunction(context.appInfo, exp.getName(), params); } @@ -2091,7 +2109,7 @@ public class FHIRPathEngine { if (b != null) res.add(b); return res; - + } private List funcAll(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException { @@ -2116,7 +2134,7 @@ public class FHIRPathEngine { boolean v = false; if (item instanceof BooleanType) { v = ((BooleanType) item).booleanValue(); - } else + } else v = item != null; if (!v) { all = false; @@ -2263,7 +2281,7 @@ public class FHIRPathEngine { private List funcIs(ExecutionContext context, List focus, ExpressionNode exp) throws PathEngineException { List result = new ArrayList(); - if (focus.size() == 0 || focus.size() > 1) + if (focus.size() == 0 || focus.size() > 1) result.add(new BooleanType(false)); else { String tn = exp.getParameters().get(0).getName(); @@ -2528,7 +2546,7 @@ public class FHIRPathEngine { s = sw.substring(i1, Math.min(sw.length(), i1+i2)); else s = sw.substring(i1); - if (!Utilities.noString(s)) + if (!Utilities.noString(s)) result.add(new StringType(s)); } return result; @@ -2697,14 +2715,14 @@ public class FHIRPathEngine { for (String rn : worker.getResourceNames()) { if (!result.hasType(worker, rn)) { getChildTypesByName(result.addType(rn), "**", result); - } + } } } else if (!result.hasType(worker, tn)) { getChildTypesByName(result.addType(tn), "**", result); } } } - } + } } else if (name.equals("*")) { assert(result.getCollectionStatus() == CollectionStatus.UNORDERED); for (ElementDefinition ed : sdi.getSnapshot().getElement()) { @@ -2767,12 +2785,12 @@ public class FHIRPathEngine { else return new ElementDefinitionMatch(ed, path.substring(ed.getPath().length()-3)); } - if (ed.getPath().contains(".") && path.startsWith(ed.getPath()+".") && (ed.getType().size() > 0) && !isAbstractType(ed.getType())) { + if (ed.getPath().contains(".") && path.startsWith(ed.getPath()+".") && (ed.getType().size() > 0) && !isAbstractType(ed.getType())) { // now we walk into the type. if (ed.getType().size() > 1) // if there's more than one type, the test above would fail this throw new PathEngineException("Internal typing issue...."); StructureDefinition nsd = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+ed.getType().get(0).getCode()); - if (nsd == null) + if (nsd == null) throw new PathEngineException("Unknown type "+ed.getType().get(0).getCode()); return getElementDefinition(nsd, nsd.getId()+path.substring(ed.getPath().length()), allowTypedName); } @@ -2790,7 +2808,7 @@ public class FHIRPathEngine { private boolean hasType(ElementDefinition ed, String s) { - for (TypeRefComponent t : ed.getType()) + for (TypeRefComponent t : ed.getType()) if (s.equalsIgnoreCase(t.getCode())) return true; return false; @@ -2802,7 +2820,7 @@ public class FHIRPathEngine { private ElementDefinitionMatch getElementDefinitionById(StructureDefinition sd, String ref) { for (ElementDefinition ed : sd.getSnapshot().getElement()) { - if (ref.equals("#"+ed.getId())) + if (ref.equals("#"+ed.getId())) return new ElementDefinitionMatch(ed, null); } return null; diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java index aad23e03bae..f1d2fb764ad 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/utils/FHIRPathEngine.java @@ -1,27 +1,59 @@ package org.hl7.fhir.r4.utils; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + import ca.uhn.fhir.model.api.TemporalPrecisionEnum; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.util.ElementUtil; -import org.fhir.ucum.Decimal; -import org.fhir.ucum.UcumException; -import org.hl7.fhir.exceptions.DefinitionException; -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.r4.conformance.ProfileUtilities; import org.hl7.fhir.r4.context.IWorkerContext; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.elementmodel.Element; +import org.hl7.fhir.r4.elementmodel.ObjectConverter; +import org.hl7.fhir.r4.model.Base; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.ElementDefinition; import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent; -import org.hl7.fhir.r4.model.ExpressionNode.*; +import org.hl7.fhir.r4.model.ExpressionNode; +import org.hl7.fhir.r4.model.ExpressionNode.CollectionStatus; +import org.hl7.fhir.r4.model.ExpressionNode.Function; +import org.hl7.fhir.r4.model.ExpressionNode.Kind; +import org.hl7.fhir.r4.model.ExpressionNode.Operation; +import org.hl7.fhir.r4.model.ExpressionNode.SourceLocation; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Property; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule; +//import org.hl7.fhir.r4.model.TemporalPrecisionEnum; +import org.hl7.fhir.r4.model.TimeType; +import org.hl7.fhir.r4.model.TypeDetails; +import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.TypeDetails.ProfiledType; import org.hl7.fhir.r4.utils.FHIRLexer.FHIRLexerException; import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext.FunctionDetails; +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.utilities.Utilities; +import org.fhir.ucum.Decimal; +import org.fhir.ucum.UcumException; -import java.math.BigDecimal; -import java.util.*; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.ElementUtil; + +import static org.apache.commons.lang3.StringUtils.length; /** * @@ -282,6 +314,21 @@ public class FHIRPathEngine { return check(appContext, resourceType, context, parse(expr)); } + private int compareDateTimeElements(Base theL, Base theR) { + String dateLeftString = theL.primitiveValue(); + if (length(dateLeftString) > 10) { + DateTimeType dateLeft = new DateTimeType(dateLeftString); + dateLeft.setTimeZoneZulu(true); + dateLeftString = dateLeft.getValueAsString(); + } + String dateRightString = theR.primitiveValue(); + if (length(dateRightString) > 10) { + DateTimeType dateRight = new DateTimeType(dateRightString); + dateRight.setTimeZoneZulu(true); + dateRightString = dateRight.getValueAsString(); + } + return dateLeftString.compareTo(dateRightString); + } /** * evaluate a path and return the matching elements @@ -1191,6 +1238,8 @@ public class FHIRPathEngine { result.addType("decimal"); return result; case Concatenate: + result = new TypeDetails(CollectionStatus.SINGLETON, ""); + return result; case Plus: result = new TypeDetails(CollectionStatus.SINGLETON); if (left.hasType(worker, "integer") && right.hasType(worker, "integer")) @@ -1267,7 +1316,7 @@ public class FHIRPathEngine { if (left.hasType("integer", "decimal", "unsignedInt", "positiveInt") && right.hasType("integer", "decimal", "unsignedInt", "positiveInt")) return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); if (left.hasType("date", "dateTime", "time", "instant") && right.hasType("date", "dateTime", "time", "instant")) - return Utilities.equivalentNumber(left.primitiveValue(), right.primitiveValue()); + return compareDateTimeElements(left, right) == 0; if (left.hasType("string", "id", "code", "uri") && right.hasType("string", "id", "code", "uri")) return Utilities.equivalent(convertToString(left), convertToString(right)); @@ -1324,8 +1373,8 @@ public class FHIRPathEngine { return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); else if ((l.hasType("integer") || l.hasType("decimal")) && (r.hasType("integer") || r.hasType("decimal"))) return makeBoolean(new Double(l.primitiveValue()) < new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) < 0); else if ((l.hasType("time")) && (r.hasType("time"))) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) < 0); } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { @@ -1348,8 +1397,8 @@ public class FHIRPathEngine { return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) return makeBoolean(new Double(l.primitiveValue()) > new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) > 0); else if ((l.hasType("time")) && (r.hasType("time"))) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) > 0); } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { @@ -1372,8 +1421,8 @@ public class FHIRPathEngine { return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) return makeBoolean(new Double(l.primitiveValue()) <= new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) <= 0); else if ((l.hasType("time")) && (r.hasType("time"))) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) <= 0); } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { @@ -1398,8 +1447,8 @@ public class FHIRPathEngine { return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); else if ((l.hasType("integer", "decimal", "unsignedInt", "positiveInt")) && (r.hasType("integer", "decimal", "unsignedInt", "positiveInt"))) return makeBoolean(new Double(l.primitiveValue()) >= new Double(r.primitiveValue())); - else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) - return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); + else if ((l.hasType("date", "dateTime", "instant")) && (r.hasType("date", "dateTime", "instant"))) + return makeBoolean(compareDateTimeElements(l, r) >= 0); else if ((l.hasType("time")) && (r.hasType("time"))) return makeBoolean(l.primitiveValue().compareTo(r.primitiveValue()) >= 0); } else if (left.size() == 1 && right.size() == 1 && left.get(0).fhirType().equals("Quantity") && right.get(0).fhirType().equals("Quantity") ) { @@ -2209,8 +2258,34 @@ public class FHIRPathEngine { } - private List funcReplace(ExecutionContext context, List focus, ExpressionNode exp) { - throw new Error("not Implemented yet"); + private List funcReplace(ExecutionContext context, List focus, ExpressionNode exp) throws FHIRException, PathEngineException { + List result = new ArrayList(); + + if (focus.size() == 1) { + String f = convertToString(focus.get(0)); + + if (!Utilities.noString(f)) { + + if (exp.getParameters().size() != 2) { + + String t = convertToString(execute(context, focus, exp.getParameters().get(0), true)); + String r = convertToString(execute(context, focus, exp.getParameters().get(1), true)); + + String n = f.replace(t, r); + result.add(new StringType(n)); + } + else { + throw new PathEngineException(String.format("funcReplace() : checking for 2 arguments (pattern, substitution) but found %d items", exp.getParameters().size())); + } + } + else { + throw new PathEngineException(String.format("funcReplace() : checking for 1 string item but found empty item")); + } + } + else { + throw new PathEngineException(String.format("funcReplace() : checking for 1 string item but found %d items", focus.size())); + } + return result; } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java index 74f3ee08498..4ddfcae54fd 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java @@ -1,6 +1,9 @@ package org.hl7.fhir.dstu3.hapi.validation; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.dstu2.composite.PeriodDt; +import ca.uhn.fhir.model.dstu2.valueset.ProcedureStatusEnum; +import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ResultSeverityEnum; @@ -64,6 +67,34 @@ public class FhirInstanceValidatorDstu3Test { myValidConcepts.add(theSystem + "___" + theCode); } + /** + * See #873 + */ + @Test + public void testCompareTimesWithDifferentTimezones() { + Procedure procedure = new Procedure(); + procedure.setStatus(Procedure.ProcedureStatus.COMPLETED); + procedure.getSubject().setReference("Patient/1"); + procedure.getCode().setText("Some proc"); + + Period period = new Period(); + period.setStartElement(new DateTimeType("2000-01-01T00:00:01+05:00")); + period.setEndElement(new DateTimeType("2000-01-01T00:00:00+04:00")); + assertThat(period.getStart().getTime(), lessThan(period.getEnd().getTime())); + procedure.setPerformed(period); + + FhirValidator val = ourCtx.newValidator(); + val.registerValidatorModule(new FhirInstanceValidator(myDefaultValidationSupport)); + + ValidationResult result = val.validateWithResult(procedure); + + String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome()); + ourLog.info(encoded); + + assertTrue(result.isSuccessful()); + } + + @SuppressWarnings("unchecked") @Before public void before() { diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/instance/hapi/validation/FhirInstanceValidatorTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/instance/hapi/validation/FhirInstanceValidatorTest.java index 2ffdf836d06..f6b9b9e7ea1 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/instance/hapi/validation/FhirInstanceValidatorTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/instance/hapi/validation/FhirInstanceValidatorTest.java @@ -1,9 +1,13 @@ package org.hl7.fhir.instance.hapi.validation; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.dstu2.composite.PeriodDt; import ca.uhn.fhir.model.dstu2.resource.Parameters; import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.dstu2.resource.Procedure; +import ca.uhn.fhir.model.dstu2.valueset.ProcedureStatusEnum; import ca.uhn.fhir.model.primitive.DateDt; +import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; @@ -23,6 +27,7 @@ import org.junit.Test; import java.io.IOException; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.not; import static org.junit.Assert.*; @@ -49,34 +54,6 @@ public class FhirInstanceValidatorTest { assertTrue(result.isSuccessful()); } - /* - * { - "resourceType": "Observation", - "meta": { - "profile": [ - "http://example.com/foo/bar/testValidateResourceContainingProfileDeclarationJson" - ] - }, - "identifier": [ - { - "system": "http://acme", - "value": "12345" - } - ], - "status": "final", - "code": { - "coding": [ - { - "system": "http://loinc.org", - "code": "12345" - } - ] - }, - "encounter": { - "reference": "http://foo.com/Encounter/9" - } -} - */ @Test public void testObservation() { Observation o = new Observation(); @@ -97,6 +74,34 @@ public class FhirInstanceValidatorTest { assertTrue(result.isSuccessful()); } + /** + * See #873 + */ + @Test + public void testCompareTimesWithDifferentTimezones() { + Procedure procedure = new Procedure(); + procedure.setStatus(ProcedureStatusEnum.COMPLETED); + procedure.getSubject().setReference("Patient/1"); + procedure.getCode().setText("Some proc"); + + PeriodDt period = new PeriodDt(); + period.setStart(new DateTimeDt("2000-01-01T00:00:01+05:00")); + period.setEnd(new DateTimeDt("2000-01-01T00:00:00+04:00")); + assertThat(period.getStart().getTime(), lessThan(period.getEnd().getTime())); + procedure.setPerformed(period); + + FhirValidator val = ourCtxDstu2.newValidator(); + + val.registerValidatorModule(ourValidator); + + ValidationResult result = val.validateWithResult(procedure); + + String encoded = ourCtxDstu2.newJsonParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome()); + ourLog.info(encoded); + + assertTrue(result.isSuccessful()); + } + @Test public void testParametersHl7OrgDstu2() { org.hl7.fhir.instance.model.Patient patient = new org.hl7.fhir.instance.model.Patient(); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index 43789e0cc34..a57c4a5088a 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -1,23 +1,8 @@ package org.hl7.fhir.r4.validation; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; -import java.util.*; -import java.util.zip.GZIPInputStream; - +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.SingleValidationMessage; @@ -25,7 +10,6 @@ import ca.uhn.fhir.validation.ValidationResult; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidatorDstu3Test; import org.hl7.fhir.dstu3.hapi.validation.ResourceValidatorDstu3Test; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -50,33 +34,248 @@ import org.junit.runner.Description; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.util.TestUtil; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.zip.GZIPInputStream; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class FhirInstanceValidatorR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidatorR4Test.class); private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport(); private static FhirContext ourCtx = FhirContext.forR4(); - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidatorR4Test.class); - private FhirInstanceValidator myInstanceVal; - private IValidationSupport myMockSupport; - - private Map mySupportedCodeSystemsForExpansion; - private FhirValidator myVal; - private ArrayList myValidConcepts; - private Set myValidSystems = new HashSet(); @Rule public TestRule watcher = new TestWatcher() { protected void starting(Description description) { ourLog.info("Starting test: " + description.getMethodName()); } }; + private FhirInstanceValidator myInstanceVal; + private IValidationSupport myMockSupport; + private Map mySupportedCodeSystemsForExpansion; + private FhirValidator myVal; + private ArrayList myValidConcepts; + private Set myValidSystems = new HashSet(); private void addValidConcept(String theSystem, String theCode) { myValidSystems.add(theSystem); myValidConcepts.add(theSystem + "___" + theCode); } + @SuppressWarnings("unchecked") + @Before + public void before() { + myVal = ourCtx.newValidator(); + myVal.setValidateAgainstStandardSchema(false); + myVal.setValidateAgainstStandardSchematron(false); + + myMockSupport = mock(IValidationSupport.class); + ValidationSupportChain validationSupport = new ValidationSupportChain(myMockSupport, myDefaultValidationSupport); + myInstanceVal = new FhirInstanceValidator(validationSupport); + + myVal.registerValidatorModule(myInstanceVal); + + mySupportedCodeSystemsForExpansion = new HashMap<>(); + + myValidConcepts = new ArrayList<>(); + + when(myMockSupport.expandValueSet(any(FhirContext.class), any(ConceptSetComponent.class))).thenAnswer(new Answer() { + @Override + public ValueSetExpansionComponent answer(InvocationOnMock theInvocation) throws Throwable { + ConceptSetComponent arg = (ConceptSetComponent) theInvocation.getArguments()[ 0 ]; + ValueSetExpansionComponent retVal = mySupportedCodeSystemsForExpansion.get(arg.getSystem()); + if (retVal == null) { + retVal = myDefaultValidationSupport.expandValueSet(any(FhirContext.class), arg); + } + ourLog.debug("expandValueSet({}) : {}", new Object[] {theInvocation.getArguments()[ 0 ], retVal}); + return retVal; + } + }); + when(myMockSupport.isCodeSystemSupported(any(FhirContext.class), any(String.class))).thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock theInvocation) throws Throwable { + boolean retVal = myValidSystems.contains(theInvocation.getArguments()[ 1 ]); + ourLog.debug("isCodeSystemSupported({}) : {}", new Object[] {theInvocation.getArguments()[ 1 ], retVal}); + return retVal; + } + }); + when(myMockSupport.fetchResource(any(FhirContext.class), any(Class.class), any(String.class))).thenAnswer(new Answer() { + @Override + public IBaseResource answer(InvocationOnMock theInvocation) throws Throwable { + IBaseResource retVal; + String id = (String) theInvocation.getArguments()[ 2 ]; + if ("Questionnaire/q_jon".equals(id)) { + retVal = ourCtx.newJsonParser().parseResource(IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/q_jon.json"))); + } else { + retVal = myDefaultValidationSupport.fetchResource((FhirContext) theInvocation.getArguments()[ 0 ], (Class) theInvocation.getArguments()[ 1 ], id); + } + ourLog.debug("fetchResource({}, {}) : {}", new Object[] {theInvocation.getArguments()[ 1 ], id, retVal}); + return retVal; + } + }); + when(myMockSupport.validateCode(any(FhirContext.class), any(String.class), any(String.class), any(String.class))).thenAnswer(new Answer() { + @Override + public CodeValidationResult answer(InvocationOnMock theInvocation) throws Throwable { + FhirContext ctx = (FhirContext) theInvocation.getArguments()[ 0 ]; + String system = (String) theInvocation.getArguments()[ 1 ]; + String code = (String) theInvocation.getArguments()[ 2 ]; + CodeValidationResult retVal; + if (myValidConcepts.contains(system + "___" + code)) { + retVal = new CodeValidationResult(new ConceptDefinitionComponent(new CodeType(code))); + } else { + retVal = myDefaultValidationSupport.validateCode(ctx, system, code, (String) theInvocation.getArguments()[ 2 ]); + } + ourLog.debug("validateCode({}, {}, {}) : {}", new Object[] {system, code, (String) theInvocation.getArguments()[ 2 ], retVal}); + return retVal; + } + }); + when(myMockSupport.fetchCodeSystem(any(FhirContext.class), any(String.class))).thenAnswer(new Answer() { + @Override + public CodeSystem answer(InvocationOnMock theInvocation) throws Throwable { + CodeSystem retVal = myDefaultValidationSupport.fetchCodeSystem((FhirContext) theInvocation.getArguments()[ 0 ], (String) theInvocation.getArguments()[ 1 ]); + ourLog.debug("fetchCodeSystem({}) : {}", new Object[] {(String) theInvocation.getArguments()[ 1 ], retVal}); + return retVal; + } + }); + when(myMockSupport.fetchStructureDefinition(any(FhirContext.class), any(String.class))).thenAnswer(new Answer() { + @Override + public StructureDefinition answer(InvocationOnMock theInvocation) throws Throwable { + StructureDefinition retVal = myDefaultValidationSupport.fetchStructureDefinition((FhirContext) theInvocation.getArguments()[ 0 ], (String) theInvocation.getArguments()[ 1 ]); + ourLog.debug("fetchStructureDefinition({}) : {}", new Object[] {(String) theInvocation.getArguments()[ 1 ], retVal}); + return retVal; + } + }); + when(myMockSupport.fetchAllStructureDefinitions(any(FhirContext.class))).thenAnswer(new Answer>() { + @Override + public List answer(InvocationOnMock theInvocation) throws Throwable { + List retVal = myDefaultValidationSupport.fetchAllStructureDefinitions((FhirContext) theInvocation.getArguments()[ 0 ]); + ourLog.debug("fetchAllStructureDefinitions()", new Object[] {}); + return retVal; + } + }); + + } + + private Object defaultString(Integer theLocationLine) { + return theLocationLine != null ? theLocationLine.toString() : ""; + } + + private StructureDefinition loadStructureDefinition(DefaultProfileValidationSupport theDefaultValSupport, String theResName) throws IOException, FHIRException { + StructureDefinition derived = ourCtx.newXmlParser().parseResource(StructureDefinition.class, IOUtils.toString(ResourceValidatorDstu3Test.class.getResourceAsStream(theResName))); + StructureDefinition base = theDefaultValSupport.fetchStructureDefinition(ourCtx, derived.getBaseDefinition()); + Validate.notNull(base); + + IWorkerContext worker = new HapiWorkerContext(ourCtx, theDefaultValSupport); + List issues = new ArrayList<>(); + ProfileUtilities profileUtilities = new ProfileUtilities(worker, issues, null); + profileUtilities.generateSnapshot(base, derived, "", ""); + + return derived; + } + + private List logResultsAndReturnAll(ValidationResult theOutput) { + List retVal = new ArrayList(); + + int index = 0; + for (SingleValidationMessage next : theOutput.getMessages()) { + ourLog.info("Result {}: {} - {}:{} {} - {}", + new Object[] {index, next.getSeverity(), defaultString(next.getLocationLine()), defaultString(next.getLocationCol()), next.getLocationString(), next.getMessage()}); + index++; + + retVal.add(next); + } + + return retVal; + } + + private List logResultsAndReturnNonInformationalOnes(ValidationResult theOutput) { + List retVal = new ArrayList(); + + int index = 0; + for (SingleValidationMessage next : theOutput.getMessages()) { + ourLog.info("Result {}: {} - {} - {}", new Object[] {index, next.getSeverity(), next.getLocationString(), next.getMessage()}); + index++; + + if (next.getSeverity() != ResultSeverityEnum.INFORMATION) { + retVal.add(next); + } + } + + return retVal; + } + + @Test + public void testBase64Invalid() { + Base64BinaryType value = new Base64BinaryType(new byte[] {2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1}); + Media med = new Media(); + med.getContent().setContentType("LCws"); + med.getContent().setDataElement(value); + med.getContent().setTitle("bbbb syst"); + med.setStatus(Media.MediaStatus.ABORTED); + String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(med); + + encoded = encoded.replace(value.getValueAsString(), "%%%2@()()"); + + ourLog.info("Encoded: {}", encoded); + + ValidationResult output = myVal.validateWithResult(encoded); + List errors = logResultsAndReturnNonInformationalOnes(output); + assertEquals(1, errors.size()); + assertEquals("The value \"%%%2@()()\" is not a valid Base64 value", errors.get(0).getMessage()); + + } + + @Test + public void testBase64Valid() { + Base64BinaryType value = new Base64BinaryType(new byte[] {2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1}); + Media med = new Media(); + med.getContent().setContentType("LCws"); + med.getContent().setDataElement(value); + med.getContent().setTitle("bbbb syst"); + med.setStatus(Media.MediaStatus.ABORTED); + String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(med); + + ourLog.info("Encoded: {}", encoded); + + ValidationResult output = myVal.validateWithResult(encoded); + List errors = logResultsAndReturnNonInformationalOnes(output); + assertEquals(0, errors.size()); + + } + + /** + * See #873 + */ + @Test + public void testCompareTimesWithDifferentTimezones() { + Procedure procedure = new Procedure(); + procedure.setStatus(Procedure.ProcedureStatus.COMPLETED); + procedure.getSubject().setReference("Patient/1"); + procedure.getCode().setText("Some proc"); + + Period period = new Period(); + period.setStartElement(new DateTimeType("2000-01-01T00:00:01+05:00")); + period.setEndElement(new DateTimeType("2000-01-01T00:00:00+04:00")); + assertThat(period.getStart().getTime(), lessThan(period.getEnd().getTime())); + procedure.setPerformed(period); + + FhirValidator val = ourCtx.newValidator(); + val.registerValidatorModule(new FhirInstanceValidator(myDefaultValidationSupport)); + + ValidationResult result = val.validateWithResult(procedure); + + String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome()); + ourLog.info(encoded); + + assertTrue(result.isSuccessful()); + } + /** * See #531 */ @@ -94,45 +293,52 @@ public class FhirInstanceValidatorR4Test { } /** - * See #370 + * See #872 */ @Test - public void testValidateRelatedPerson() { + public void testExtensionUrlWithHl7Url() throws IOException { + String input = IOUtils.toString(FhirInstanceValidator.class.getResourceAsStream("/bug872-ext-with-hl7-url.json"), Charsets.UTF_8); + ValidationResult output = myVal.validateWithResult(input); + List nonInfo = logResultsAndReturnNonInformationalOnes(output); + assertThat(nonInfo, empty()); + } - /* - * Try with a code that is in http://hl7.org/fhir/ValueSet/relatedperson-relationshiptype - * and therefore should validate - */ - RelatedPerson rp = new RelatedPerson(); - rp.getPatient().setReference("Patient/1"); - rp.addRelationship().addCoding().setSystem("http://hl7.org/fhir/v2/0131").setCode("c"); + @Test + public void testIsNoTerminologyChecks() { + assertFalse(myInstanceVal.isNoTerminologyChecks()); + myInstanceVal.setNoTerminologyChecks(true); + assertTrue(myInstanceVal.isNoTerminologyChecks()); + } - ValidationResult results = myVal.validateWithResult(rp); - List outcome = logResultsAndReturnNonInformationalOnes(results); - assertThat(outcome, empty()); + @Test + public void testLargeBase64() throws IOException { + String input = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/r4/diagnosticreport-example-gingival-mass.json"), Constants.CHARSET_UTF8); + ValidationResult output = myVal.validateWithResult(input); + List errors = logResultsAndReturnNonInformationalOnes(output); + assertEquals(0, errors.size()); + } - /* - * Code system is case insensitive, so try with capital C - */ - rp = new RelatedPerson(); - rp.getPatient().setReference("Patient/1"); - rp.addRelationship().addCoding().setSystem("http://hl7.org/fhir/v2/0131").setCode("C"); + @Test + @Ignore + public void testValidateBigRawJsonResource() throws Exception { + InputStream stream = FhirInstanceValidatorR4Test.class.getResourceAsStream("/conformance.json.gz"); + stream = new GZIPInputStream(stream); + String input = IOUtils.toString(stream); - results = myVal.validateWithResult(rp); - outcome = logResultsAndReturnNonInformationalOnes(results); - assertThat(outcome, empty()); + long start = System.currentTimeMillis(); + ValidationResult output = null; + int passes = 1; + for (int i = 0; i < passes; i++) { + ourLog.info("Pass {}", i + 1); + output = myVal.validateWithResult(input); + } - /* - * Now a bad code - */ - rp = new RelatedPerson(); - rp.getPatient().setReference("Patient/1"); - rp.addRelationship().addCoding().setSystem("http://hl7.org/fhir/v2/0131").setCode("GAGAGAGA"); + long delay = System.currentTimeMillis() - start; + long per = delay / passes; - results = myVal.validateWithResult(rp); - outcome = logResultsAndReturnNonInformationalOnes(results); - assertThat(outcome, not(empty())); + logResultsAndReturnAll(output); + ourLog.info("Took {} ms -- {}ms / pass", delay, per); } @Test @@ -179,6 +385,14 @@ public class FhirInstanceValidatorR4Test { ourLog.info("Validated the following:\n{}", ids); } + @Test + public void testValidateBundleWithNoType() throws Exception { + String vsContents = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/r4/bundle-with-no-type.json"), "UTF-8"); + + ValidationResult output = myVal.validateWithResult(vsContents); + logResultsAndReturnNonInformationalOnes(output); + assertThat(output.getMessages().toString(), containsString("Element 'Bundle.type': minimum required = 1")); + } @Test @Ignore @@ -209,56 +423,8 @@ public class FhirInstanceValidatorR4Test { } - @Test - public void testBase64Invalid() { - Base64BinaryType value = new Base64BinaryType(new byte[]{2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1}); - Media med = new Media(); - med.getContent().setContentType("LCws"); - med.getContent().setDataElement(value); - med.getContent().setTitle("bbbb syst"); - med.setStatus(Media.MediaStatus.ABORTED); - String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(med); - - encoded = encoded.replace(value.getValueAsString(), "%%%2@()()"); - - ourLog.info("Encoded: {}", encoded); - - ValidationResult output = myVal.validateWithResult(encoded); - List errors = logResultsAndReturnNonInformationalOnes(output); - assertEquals(1, errors.size()); - assertEquals("The value \"%%%2@()()\" is not a valid Base64 value", errors.get(0).getMessage()); - - } - - @Test - public void testBase64Valid() { - Base64BinaryType value = new Base64BinaryType(new byte[]{2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1}); - Media med = new Media(); - med.getContent().setContentType("LCws"); - med.getContent().setDataElement(value); - med.getContent().setTitle("bbbb syst"); - med.setStatus(Media.MediaStatus.ABORTED); - String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(med); - - ourLog.info("Encoded: {}", encoded); - - ValidationResult output = myVal.validateWithResult(encoded); - List errors = logResultsAndReturnNonInformationalOnes(output); - assertEquals(0, errors.size()); - - } - - @Test - public void testLargeBase64() throws IOException { - String input = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/r4/diagnosticreport-example-gingival-mass.json"), Constants.CHARSET_UTF8); - ValidationResult output = myVal.validateWithResult(input); - List errors = logResultsAndReturnNonInformationalOnes(output); - assertEquals(0, errors.size()); - } - - - @Test @Ignore + @Ignore public void testValidateDocument() throws Exception { String vsContents = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/sample-document.xml"), "UTF-8"); @@ -267,194 +433,40 @@ public class FhirInstanceValidatorR4Test { assertTrue(output.isSuccessful()); } - /** - * A reference with only an identifier should be valid - */ @Test - public void testValidateReferenceWithIdentifierValid() throws Exception { - Patient p = new Patient(); - p.getManagingOrganization().getIdentifier().setSystem("http://acme.org"); - p.getManagingOrganization().getIdentifier().setValue("foo"); - - ValidationResult output = myVal.validateWithResult(p); - List nonInfo = logResultsAndReturnNonInformationalOnes(output); - assertThat(nonInfo, empty()); - } + public void testValidateProfileWithExtension() throws IOException, FHIRException { + PrePopulatedValidationSupport valSupport = new PrePopulatedValidationSupport(); + DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(); + ValidationSupportChain support = new ValidationSupportChain(valSupport, defaultSupport); - /** - * A reference with only an identifier should be valid - */ - @Test - public void testValidateReferenceWithDisplayValid() throws Exception { - Patient p = new Patient(); - p.getManagingOrganization().setDisplay("HELLO"); - - ValidationResult output = myVal.validateWithResult(p); - List nonInfo = logResultsAndReturnNonInformationalOnes(output); - assertThat(nonInfo, empty()); - } + // Prepopulate SDs + valSupport.addStructureDefinition(loadStructureDefinition(defaultSupport, "/dstu3/myconsent-profile.xml")); + valSupport.addStructureDefinition(loadStructureDefinition(defaultSupport, "/dstu3/myconsent-ext.xml")); - /** - * See #872 - */ - @Test - public void testExtensionUrlWithHl7Url() throws IOException { - String input = IOUtils.toString(FhirInstanceValidator.class.getResourceAsStream("/bug872-ext-with-hl7-url.json"), Charsets.UTF_8); - ValidationResult output = myVal.validateWithResult(input); - List nonInfo = logResultsAndReturnNonInformationalOnes(output); - assertThat(nonInfo, empty()); - } + FhirValidator val = ourCtx.newValidator(); + val.registerValidatorModule(new FhirInstanceValidator(support)); - @SuppressWarnings("unchecked") - @Before - public void before() { - myVal = ourCtx.newValidator(); - myVal.setValidateAgainstStandardSchema(false); - myVal.setValidateAgainstStandardSchematron(false); + Consent input = ourCtx.newJsonParser().parseResource(Consent.class, IOUtils.toString(ResourceValidatorDstu3Test.class.getResourceAsStream("/dstu3/myconsent-resource.json"))); + input.getPolicyRule().addCoding().setSystem("http://hl7.org/fhir/v3/ActCode").setCode("EMRGONLY"); - myMockSupport = mock(IValidationSupport.class); - ValidationSupportChain validationSupport = new ValidationSupportChain(myMockSupport, myDefaultValidationSupport); - myInstanceVal = new FhirInstanceValidator(validationSupport); + input.setScope(Consent.ConsentScope.ADR); + input.addCategory().setText("FOO"); + input.getProvision().addCode().setText("BAR"); - myVal.registerValidatorModule(myInstanceVal); + // Should pass + ValidationResult output = val.validateWithResult(input); + List all = logResultsAndReturnNonInformationalOnes(output); + assertEquals(0, all.size()); + assertEquals(0, output.getMessages().size()); - mySupportedCodeSystemsForExpansion = new HashMap<>(); + // Now with the wrong datatype + input.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/PruebaExtension").get(0).setValue(new CodeType("AAA")); - myValidConcepts = new ArrayList<>(); + // Should fail + output = val.validateWithResult(input); + all = logResultsAndReturnNonInformationalOnes(output); + assertThat(all.toString(), containsString("definition allows for the types [string] but found type code")); - when(myMockSupport.expandValueSet(any(FhirContext.class), any(ConceptSetComponent.class))).thenAnswer(new Answer() { - @Override - public ValueSetExpansionComponent answer(InvocationOnMock theInvocation) throws Throwable { - ConceptSetComponent arg = (ConceptSetComponent) theInvocation.getArguments()[0]; - ValueSetExpansionComponent retVal = mySupportedCodeSystemsForExpansion.get(arg.getSystem()); - if (retVal == null) { - retVal = myDefaultValidationSupport.expandValueSet(any(FhirContext.class), arg); - } - ourLog.debug("expandValueSet({}) : {}", new Object[] { theInvocation.getArguments()[0], retVal }); - return retVal; - } - }); - when(myMockSupport.isCodeSystemSupported(any(FhirContext.class), any(String.class))).thenAnswer(new Answer() { - @Override - public Boolean answer(InvocationOnMock theInvocation) throws Throwable { - boolean retVal = myValidSystems.contains(theInvocation.getArguments()[1]); - ourLog.debug("isCodeSystemSupported({}) : {}", new Object[] { theInvocation.getArguments()[1], retVal }); - return retVal; - } - }); - when(myMockSupport.fetchResource(any(FhirContext.class), any(Class.class), any(String.class))).thenAnswer(new Answer() { - @Override - public IBaseResource answer(InvocationOnMock theInvocation) throws Throwable { - IBaseResource retVal; - String id = (String) theInvocation.getArguments()[2]; - if ("Questionnaire/q_jon".equals(id)) { - retVal = ourCtx.newJsonParser().parseResource(IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/q_jon.json"))); - } else { - retVal = myDefaultValidationSupport.fetchResource((FhirContext) theInvocation.getArguments()[0], (Class) theInvocation.getArguments()[1], id); - } - ourLog.debug("fetchResource({}, {}) : {}", new Object[] { theInvocation.getArguments()[1], id, retVal }); - return retVal; - } - }); - when(myMockSupport.validateCode(any(FhirContext.class), any(String.class), any(String.class), any(String.class))).thenAnswer(new Answer() { - @Override - public CodeValidationResult answer(InvocationOnMock theInvocation) throws Throwable { - FhirContext ctx = (FhirContext) theInvocation.getArguments()[0]; - String system = (String) theInvocation.getArguments()[1]; - String code = (String) theInvocation.getArguments()[2]; - CodeValidationResult retVal; - if (myValidConcepts.contains(system + "___" + code)) { - retVal = new CodeValidationResult(new ConceptDefinitionComponent(new CodeType(code))); - } else { - retVal = myDefaultValidationSupport.validateCode(ctx, system, code, (String) theInvocation.getArguments()[2]); - } - ourLog.debug("validateCode({}, {}, {}) : {}", new Object[] { system, code, (String) theInvocation.getArguments()[2], retVal }); - return retVal; - } - }); - when(myMockSupport.fetchCodeSystem(any(FhirContext.class), any(String.class))).thenAnswer(new Answer() { - @Override - public CodeSystem answer(InvocationOnMock theInvocation) throws Throwable { - CodeSystem retVal = myDefaultValidationSupport.fetchCodeSystem((FhirContext) theInvocation.getArguments()[0], (String) theInvocation.getArguments()[1]); - ourLog.debug("fetchCodeSystem({}) : {}", new Object[] { (String) theInvocation.getArguments()[1], retVal }); - return retVal; - } - }); - when(myMockSupport.fetchStructureDefinition(any(FhirContext.class), any(String.class))).thenAnswer(new Answer() { - @Override - public StructureDefinition answer(InvocationOnMock theInvocation) throws Throwable { - StructureDefinition retVal = myDefaultValidationSupport.fetchStructureDefinition((FhirContext) theInvocation.getArguments()[0], (String) theInvocation.getArguments()[1]); - ourLog.debug("fetchStructureDefinition({}) : {}", new Object[] { (String) theInvocation.getArguments()[1], retVal }); - return retVal; - } - }); - when(myMockSupport.fetchAllStructureDefinitions(any(FhirContext.class))).thenAnswer(new Answer>() { - @Override - public List answer(InvocationOnMock theInvocation) throws Throwable { - List retVal = myDefaultValidationSupport.fetchAllStructureDefinitions((FhirContext) theInvocation.getArguments()[0]); - ourLog.debug("fetchAllStructureDefinitions()", new Object[] {}); - return retVal; - } - }); - - } - - private Object defaultString(Integer theLocationLine) { - return theLocationLine != null ? theLocationLine.toString() : ""; - } - - private List logResultsAndReturnAll(ValidationResult theOutput) { - List retVal = new ArrayList(); - - int index = 0; - for (SingleValidationMessage next : theOutput.getMessages()) { - ourLog.info("Result {}: {} - {}:{} {} - {}", - new Object[] { index, next.getSeverity(), defaultString(next.getLocationLine()), defaultString(next.getLocationCol()), next.getLocationString(), next.getMessage() }); - index++; - - retVal.add(next); - } - - return retVal; - } - - private List logResultsAndReturnNonInformationalOnes(ValidationResult theOutput) { - List retVal = new ArrayList(); - - int index = 0; - for (SingleValidationMessage next : theOutput.getMessages()) { - ourLog.info("Result {}: {} - {} - {}", new Object[] { index, next.getSeverity(), next.getLocationString(), next.getMessage() }); - index++; - - if (next.getSeverity() != ResultSeverityEnum.INFORMATION) { - retVal.add(next); - } - } - - return retVal; - } - - @Test - @Ignore - public void testValidateBigRawJsonResource() throws Exception { - InputStream stream = FhirInstanceValidatorR4Test.class.getResourceAsStream("/conformance.json.gz"); - stream = new GZIPInputStream(stream); - String input = IOUtils.toString(stream); - - long start = System.currentTimeMillis(); - ValidationResult output = null; - int passes = 1; - for (int i = 0; i < passes; i++) { - ourLog.info("Pass {}", i + 1); - output = myVal.validateWithResult(input); - } - - long delay = System.currentTimeMillis() - start; - long per = delay / passes; - - logResultsAndReturnAll(output); - - ourLog.info("Took {} ms -- {}ms / pass", delay, per); } @Test @@ -478,6 +490,41 @@ public class FhirInstanceValidatorR4Test { assertEquals(output.toString(), 0, output.getMessages().size()); } + @Test + public void testValidateRawJsonResourceBadAttributes() { + //@formatter:off + String input = + "{" + + "\"resourceType\":\"Patient\"," + + "\"id\":\"123\"," + + "\"foo\":\"123\"" + + "}"; + //@formatter:on + + ValidationResult output = myVal.validateWithResult(input); + assertEquals(output.toString(), 1, output.getMessages().size()); + ourLog.info(output.getMessages().get(0).getLocationString()); + ourLog.info(output.getMessages().get(0).getMessage()); + assertEquals("/Patient", output.getMessages().get(0).getLocationString()); + assertEquals("Unrecognised property '@foo'", output.getMessages().get(0).getMessage()); + } + + @Test + @Ignore + public void testValidateRawJsonResourceFromExamples() throws Exception { + // @formatter:off + String input = IOUtils.toString(FhirInstanceValidator.class.getResourceAsStream("/testscript-search.json")); + // @formatter:on + + ValidationResult output = myVal.validateWithResult(input); + logResultsAndReturnNonInformationalOnes(output); + // assertEquals(output.toString(), 1, output.getMessages().size()); + // ourLog.info(output.getMessages().get(0).getLocationString()); + // ourLog.info(output.getMessages().get(0).getMessage()); + // assertEquals("/foo", output.getMessages().get(0).getLocationString()); + // assertEquals("Element is unknown or does not match any slice", output.getMessages().get(0).getMessage()); + } + @Test public void testValidateRawJsonResourceWithUnknownExtension() { @@ -545,66 +592,6 @@ public class FhirInstanceValidatorR4Test { assertEquals(ResultSeverityEnum.ERROR, output.getMessages().get(0).getSeverity()); } - @Test - public void testValidateRawXmlWithMissingRootNamespace() { - //@formatter:off - String input = "" - + "" - + " " - + " " - + "
Some narrative
" - + "
" - + " " - + " " - + " " - + " " - + " " - + " " - + " " - + "
"; - //@formatter:on - - ValidationResult output = myVal.validateWithResult(input); - assertEquals(output.toString(), 1, output.getMessages().size()); - assertEquals("This cannot be parsed as a FHIR object (no namespace)", output.getMessages().get(0).getMessage()); - ourLog.info(output.getMessages().get(0).getLocationString()); - } - - @Test - public void testValidateRawJsonResourceBadAttributes() { - //@formatter:off - String input = - "{" + - "\"resourceType\":\"Patient\"," + - "\"id\":\"123\"," + - "\"foo\":\"123\"" + - "}"; - //@formatter:on - - ValidationResult output = myVal.validateWithResult(input); - assertEquals(output.toString(), 1, output.getMessages().size()); - ourLog.info(output.getMessages().get(0).getLocationString()); - ourLog.info(output.getMessages().get(0).getMessage()); - assertEquals("/Patient", output.getMessages().get(0).getLocationString()); - assertEquals("Unrecognised property '@foo'", output.getMessages().get(0).getMessage()); - } - - @Test - @Ignore - public void testValidateRawJsonResourceFromExamples() throws Exception { - // @formatter:off - String input = IOUtils.toString(FhirInstanceValidator.class.getResourceAsStream("/testscript-search.json")); - // @formatter:on - - ValidationResult output = myVal.validateWithResult(input); - logResultsAndReturnNonInformationalOnes(output); - // assertEquals(output.toString(), 1, output.getMessages().size()); - // ourLog.info(output.getMessages().get(0).getLocationString()); - // ourLog.info(output.getMessages().get(0).getMessage()); - // assertEquals("/foo", output.getMessages().get(0).getLocationString()); - // assertEquals("Element is unknown or does not match any slice", output.getMessages().get(0).getMessage()); - } - @Test public void testValidateRawXmlResource() { // @formatter:off @@ -616,15 +603,20 @@ public class FhirInstanceValidatorR4Test { } @Test - public void testValidateBundleWithNoType() throws Exception { - String vsContents = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/r4/bundle-with-no-type.json"), "UTF-8"); + public void testValidateRawXmlResourceBadAttributes() { + //@formatter:off + String input = "" + "" + "" + + ""; + //@formatter:on - ValidationResult output = myVal.validateWithResult(vsContents); - logResultsAndReturnNonInformationalOnes(output); - assertThat(output.getMessages().toString(), containsString("Element 'Bundle.type': minimum required = 1")); + ValidationResult output = myVal.validateWithResult(input); + assertEquals(output.toString(), 1, output.getMessages().size()); + ourLog.info(output.getMessages().get(0).getLocationString()); + ourLog.info(output.getMessages().get(0).getMessage()); + assertEquals("/f:Patient", output.getMessages().get(0).getLocationString()); + assertEquals("Undefined element 'foo'", output.getMessages().get(0).getMessage()); } - @Test public void testValidateRawXmlResourceWithEmptyPrimitive() { // @formatter:off @@ -642,26 +634,26 @@ public class FhirInstanceValidatorR4Test { @Test public void testValidateRawXmlResourceWithPrimitiveContainingOnlyAnExtension() { // @formatter:off - String input = "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " "; + String input = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " "; // @formatter:on ValidationResult output = myVal.validateWithResult(input); @@ -670,18 +662,97 @@ public class FhirInstanceValidatorR4Test { } @Test - public void testValidateRawXmlResourceBadAttributes() { + public void testValidateRawXmlWithMissingRootNamespace() { //@formatter:off - String input = "" + "" + "" - + ""; - //@formatter:on + String input = "" + + "" + + " " + + " " + + "
Some narrative
" + + "
" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "
"; + //@formatter:on ValidationResult output = myVal.validateWithResult(input); assertEquals(output.toString(), 1, output.getMessages().size()); + assertEquals("This cannot be parsed as a FHIR object (no namespace)", output.getMessages().get(0).getMessage()); ourLog.info(output.getMessages().get(0).getLocationString()); - ourLog.info(output.getMessages().get(0).getMessage()); - assertEquals("/f:Patient", output.getMessages().get(0).getLocationString()); - assertEquals("Undefined element 'foo'", output.getMessages().get(0).getMessage()); + } + + /** + * A reference with only an identifier should be valid + */ + @Test + public void testValidateReferenceWithDisplayValid() throws Exception { + Patient p = new Patient(); + p.getManagingOrganization().setDisplay("HELLO"); + + ValidationResult output = myVal.validateWithResult(p); + List nonInfo = logResultsAndReturnNonInformationalOnes(output); + assertThat(nonInfo, empty()); + } + + /** + * A reference with only an identifier should be valid + */ + @Test + public void testValidateReferenceWithIdentifierValid() throws Exception { + Patient p = new Patient(); + p.getManagingOrganization().getIdentifier().setSystem("http://acme.org"); + p.getManagingOrganization().getIdentifier().setValue("foo"); + + ValidationResult output = myVal.validateWithResult(p); + List nonInfo = logResultsAndReturnNonInformationalOnes(output); + assertThat(nonInfo, empty()); + } + + /** + * See #370 + */ + @Test + public void testValidateRelatedPerson() { + + /* + * Try with a code that is in http://hl7.org/fhir/ValueSet/relatedperson-relationshiptype + * and therefore should validate + */ + RelatedPerson rp = new RelatedPerson(); + rp.getPatient().setReference("Patient/1"); + rp.addRelationship().addCoding().setSystem("http://hl7.org/fhir/v2/0131").setCode("c"); + + ValidationResult results = myVal.validateWithResult(rp); + List outcome = logResultsAndReturnNonInformationalOnes(results); + assertThat(outcome, empty()); + + /* + * Code system is case insensitive, so try with capital C + */ + rp = new RelatedPerson(); + rp.getPatient().setReference("Patient/1"); + rp.addRelationship().addCoding().setSystem("http://hl7.org/fhir/v2/0131").setCode("C"); + + results = myVal.validateWithResult(rp); + outcome = logResultsAndReturnNonInformationalOnes(results); + assertThat(outcome, empty()); + + /* + * Now a bad code + */ + rp = new RelatedPerson(); + rp.getPatient().setReference("Patient/1"); + rp.addRelationship().addCoding().setSystem("http://hl7.org/fhir/v2/0131").setCode("GAGAGAGA"); + + results = myVal.validateWithResult(rp); + outcome = logResultsAndReturnNonInformationalOnes(results); + assertThat(outcome, not(empty())); + } @Test @@ -773,19 +844,19 @@ public class FhirInstanceValidatorR4Test { @Test public void testValidateResourceWithDefaultValuesetBadCode() { //@formatter:off - String input = + String input = "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - ""; + " \n" + + " \n" + + " \n" + + " \n" + + ""; //@formatter:on ValidationResult output = myVal.validateWithResult(input); logResultsAndReturnAll(output); assertEquals( - "The value provided ('notvalidcode') is not in the value set http://hl7.org/fhir/ValueSet/observation-status (http://hl7.org/fhir/ValueSet/observation-status, and a code is required from this value set) (error message = Unknown code[notvalidcode] in system[null])", - output.getMessages().get(0).getMessage()); + "The value provided ('notvalidcode') is not in the value set http://hl7.org/fhir/ValueSet/observation-status (http://hl7.org/fhir/ValueSet/observation-status, and a code is required from this value set) (error message = Unknown code[notvalidcode] in system[null])", + output.getMessages().get(0).getMessage()); } @Test @@ -803,6 +874,23 @@ public class FhirInstanceValidatorR4Test { } + @Test + public void testValidateResourceWithExampleBindingCodeValidationFailingNonLoinc() { + Observation input = new Observation(); + + myInstanceVal.setValidationSupport(myMockSupport); + addValidConcept("http://acme.org", "12345"); + + input.setStatus(ObservationStatus.FINAL); + input.getCode().addCoding().setSystem("http://acme.org").setCode("9988877"); + + ValidationResult output = myVal.validateWithResult(input); + List errors = logResultsAndReturnAll(output); + assertThat(errors.toString(), errors.size(), greaterThan(0)); + assertEquals("Unknown code: http://acme.org / 9988877", errors.get(0).getMessage()); + + } + @Test public void testValidateResourceWithExampleBindingCodeValidationPassingLoinc() { Observation input = new Observation(); @@ -853,23 +941,6 @@ public class FhirInstanceValidatorR4Test { assertEquals(errors.toString(), 0, errors.size()); } - @Test - public void testValidateResourceWithExampleBindingCodeValidationFailingNonLoinc() { - Observation input = new Observation(); - - myInstanceVal.setValidationSupport(myMockSupport); - addValidConcept("http://acme.org", "12345"); - - input.setStatus(ObservationStatus.FINAL); - input.getCode().addCoding().setSystem("http://acme.org").setCode("9988877"); - - ValidationResult output = myVal.validateWithResult(input); - List errors = logResultsAndReturnAll(output); - assertThat(errors.toString(), errors.size(), greaterThan(0)); - assertEquals("Unknown code: http://acme.org / 9988877", errors.get(0).getMessage()); - - } - @Test public void testValidateResourceWithValuesetExpansionBad() { @@ -881,8 +952,8 @@ public class FhirInstanceValidatorR4Test { assertEquals(1, all.size()); assertEquals("Patient.identifier.type", all.get(0).getLocationString()); assertEquals( - "None of the codes provided are in the value set http://hl7.org/fhir/ValueSet/identifier-type (http://hl7.org/fhir/ValueSet/identifier-type, and a code should come from this value set unless it has no suitable code) (codes = http://example.com/foo/bar#bar)", - all.get(0).getMessage()); + "None of the codes provided are in the value set http://hl7.org/fhir/ValueSet/identifier-type (http://hl7.org/fhir/ValueSet/identifier-type, and a code should come from this value set unless it has no suitable code) (codes = http://example.com/foo/bar#bar)", + all.get(0).getMessage()); assertEquals(ResultSeverityEnum.WARNING, all.get(0).getSeverity()); } @@ -897,13 +968,6 @@ public class FhirInstanceValidatorR4Test { assertEquals(0, all.size()); } - @Test - public void testIsNoTerminologyChecks() { - assertFalse(myInstanceVal.isNoTerminologyChecks()); - myInstanceVal.setNoTerminologyChecks(true); - assertTrue(myInstanceVal.isNoTerminologyChecks()); - } - @Test @Ignore public void testValidateStructureDefinition() throws IOException { @@ -924,54 +988,5 @@ public class FhirInstanceValidatorR4Test { TestUtil.clearAllStaticFieldsForUnitTest(); } - @Test - public void testValidateProfileWithExtension() throws IOException, FHIRException { - PrePopulatedValidationSupport valSupport = new PrePopulatedValidationSupport(); - DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(); - ValidationSupportChain support = new ValidationSupportChain(valSupport, defaultSupport); - - // Prepopulate SDs - valSupport.addStructureDefinition(loadStructureDefinition(defaultSupport, "/dstu3/myconsent-profile.xml")); - valSupport.addStructureDefinition(loadStructureDefinition(defaultSupport, "/dstu3/myconsent-ext.xml")); - - FhirValidator val = ourCtx.newValidator(); - val.registerValidatorModule(new FhirInstanceValidator(support)); - - Consent input = ourCtx.newJsonParser().parseResource(Consent.class, IOUtils.toString(ResourceValidatorDstu3Test.class.getResourceAsStream("/dstu3/myconsent-resource.json"))); - input.getPolicyRule().addCoding().setSystem("http://hl7.org/fhir/v3/ActCode").setCode("EMRGONLY"); - - input.setScope(Consent.ConsentScope.ADR); - input.addCategory().setText("FOO"); - input.getProvision().addCode().setText("BAR"); - - // Should pass - ValidationResult output = val.validateWithResult(input); - List all = logResultsAndReturnNonInformationalOnes(output); - assertEquals(0, all.size()); - assertEquals(0, output.getMessages().size()); - - // Now with the wrong datatype - input.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/PruebaExtension").get(0).setValue(new CodeType("AAA")); - - // Should fail - output = val.validateWithResult(input); - all = logResultsAndReturnNonInformationalOnes(output); - assertThat(all.toString(), containsString("definition allows for the types [string] but found type code")); - - } - - private StructureDefinition loadStructureDefinition(DefaultProfileValidationSupport theDefaultValSupport, String theResName) throws IOException, FHIRException { - StructureDefinition derived = ourCtx.newXmlParser().parseResource(StructureDefinition.class, IOUtils.toString(ResourceValidatorDstu3Test.class.getResourceAsStream(theResName))); - StructureDefinition base = theDefaultValSupport.fetchStructureDefinition(ourCtx, derived.getBaseDefinition()); - Validate.notNull(base); - - IWorkerContext worker = new HapiWorkerContext(ourCtx, theDefaultValSupport); - List issues = new ArrayList<>(); - ProfileUtilities profileUtilities = new ProfileUtilities(worker, issues, null); - profileUtilities.generateSnapshot(base, derived, "", ""); - - return derived; - } - }