Update R4 FHIRPath implementation from R5, and get all tests passing

This commit is contained in:
Grahame Grieve 2023-01-10 07:16:08 +11:00
parent 8902898428
commit 6725563600
13 changed files with 3358 additions and 1417 deletions

View File

@ -849,4 +849,8 @@ private Map<String, Object> userData;
return null;
}
public XhtmlNode getXhtml() {
return null;
}
}

View File

@ -45,7 +45,9 @@ import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import ca.uhn.fhir.parser.DataFormatException;
import org.hl7.fhir.utilities.DateTimeUtil;
import org.hl7.fhir.utilities.Utilities;
public abstract class BaseDateTimeType extends PrimitiveType<Date> {
@ -286,6 +288,13 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
return getFieldValue(Calendar.MONTH);
}
public float getSecondsMilli() {
int sec = getSecond();
int milli = getMillis();
String s = Integer.toString(sec)+"."+Utilities.padLeft(Integer.toString(milli), '0', 3);
return Float.parseFloat(s);
}
/**
* Returns the nanoseconds within the current second
* <p>
@ -867,84 +876,157 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
* </ul>
*/
public Boolean equalsUsingFhirPathRules(BaseDateTimeType theOther) {
BaseDateTimeType me = this;
// Per FHIRPath rules, we compare equivalence at the lowest precision of the two values,
// so if we need to, we'll clone either side and reduce its precision
int lowestPrecision = Math.min(me.getPrecision().ordinal(), theOther.getPrecision().ordinal());
TemporalPrecisionEnum lowestPrecisionEnum = TemporalPrecisionEnum.values()[lowestPrecision];
if (me.getPrecision() != lowestPrecisionEnum) {
me = new DateTimeType(me.getValueAsString());
me.setPrecision(lowestPrecisionEnum);
}
if (theOther.getPrecision() != lowestPrecisionEnum) {
theOther = new DateTimeType(theOther.getValueAsString());
theOther.setPrecision(lowestPrecisionEnum);
}
if (me.hasTimezoneIfRequired() != theOther.hasTimezoneIfRequired()) {
if (me.getPrecision() == theOther.getPrecision()) {
if (me.getPrecision().ordinal() >= TemporalPrecisionEnum.MINUTE.ordinal() && theOther.getPrecision().ordinal() >= TemporalPrecisionEnum.MINUTE.ordinal()) {
boolean couldBeTheSameTime = couldBeTheSameTime(me, theOther) || couldBeTheSameTime(theOther, me);
if (!couldBeTheSameTime) {
return false;
}
}
}
return null;
}
// Same precision
if (me.getPrecision() == theOther.getPrecision()) {
if (me.getPrecision().ordinal() >= TemporalPrecisionEnum.MINUTE.ordinal()) {
long leftTime = me.getValue().getTime();
long rightTime = theOther.getValue().getTime();
return leftTime == rightTime;
if (hasTimezone() != theOther.hasTimezone()) {
if (!couldBeTheSameTime(this, theOther)) {
return false;
} else {
String leftTime = me.getValueAsString();
String rightTime = theOther.getValueAsString();
return leftTime.equals(rightTime);
return null;
}
}
// Both represent 0 millis but the millis are optional
if (((Integer)0).equals(me.getMillis())) {
if (((Integer)0).equals(theOther.getMillis())) {
if (me.getPrecision().ordinal() >= TemporalPrecisionEnum.SECOND.ordinal()) {
if (theOther.getPrecision().ordinal() >= TemporalPrecisionEnum.SECOND.ordinal()) {
return me.getValue().getTime() == theOther.getValue().getTime();
}
}
} else {
BaseDateTimeType left = (BaseDateTimeType) this.copy();
BaseDateTimeType right = (BaseDateTimeType) theOther.copy();
if (left.hasTimezone() && left.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
left.setTimeZoneZulu(true);
}
}
return false;
if (right.hasTimezone() && right.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
right.setTimeZoneZulu(true);
}
Integer i = compareTimes(left, right, null);
return i == null ? null : i == 0;
}
}
private boolean couldBeTheSameTime(BaseDateTimeType theArg1, BaseDateTimeType theArg2) {
boolean theCouldBeTheSameTime = false;
if (theArg1.getTimeZone() == null && theArg2.getTimeZone() != null) {
long lowLeft = new DateTimeType(theArg1.getValueAsString()+"Z").getValue().getTime() - (14 * DateUtils.MILLIS_PER_HOUR);
long highLeft = new DateTimeType(theArg1.getValueAsString()+"Z").getValue().getTime() + (14 * DateUtils.MILLIS_PER_HOUR);
long right = theArg2.getValue().getTime();
if (right >= lowLeft && right <= highLeft) {
theCouldBeTheSameTime = true;
}
}
return theCouldBeTheSameTime;
private boolean couldBeTheSameTime(BaseDateTimeType theArg1, BaseDateTimeType theArg2) {
long lowLeft = theArg1.getValue().getTime();
long highLeft = theArg1.getHighEdge().getValue().getTime();
if (!theArg1.hasTimezone()) {
lowLeft = lowLeft - (14 * DateUtils.MILLIS_PER_HOUR);
highLeft = highLeft + (14 * DateUtils.MILLIS_PER_HOUR);
}
long lowRight = theArg2.getValue().getTime();
long highRight = theArg2.getHighEdge().getValue().getTime();
if (!theArg2.hasTimezone()) {
lowRight = lowRight - (14 * DateUtils.MILLIS_PER_HOUR);
highRight = highRight + (14 * DateUtils.MILLIS_PER_HOUR);
}
if (highRight < lowLeft) {
return false;
}
if (highLeft < lowRight) {
return false;
}
return true;
}
private BaseDateTimeType getHighEdge() {
BaseDateTimeType result = (BaseDateTimeType) copy();
switch (getPrecision()) {
case DAY:
result.add(Calendar.DATE, 1);
break;
case MILLI:
break;
case MINUTE:
result.add(Calendar.MINUTE, 1);
break;
case MONTH:
result.add(Calendar.MONTH, 1);
break;
case SECOND:
result.add(Calendar.SECOND, 1);
break;
case YEAR:
result.add(Calendar.YEAR, 1);
break;
default:
break;
}
return result;
}
boolean hasTimezoneIfRequired() {
return getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal() ||
getTimeZone() != null;
}
return getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal() ||
getTimeZone() != null;
}
@Override
public String fpValue() {
return "@"+primitiveValue();
}
boolean hasTimezone() {
return getTimeZone() != null;
}
public static Integer compareTimes(BaseDateTimeType left, BaseDateTimeType right, Integer def) {
if (left.getYear() < right.getYear()) {
return -1;
} else if (left.getYear() > right.getYear()) {
return 1;
} else if (left.getPrecision() == TemporalPrecisionEnum.YEAR && right.getPrecision() == TemporalPrecisionEnum.YEAR) {
return 0;
} else if (left.getPrecision() == TemporalPrecisionEnum.YEAR || right.getPrecision() == TemporalPrecisionEnum.YEAR) {
return def;
}
if (left.getMonth() < right.getMonth()) {
return -1;
} else if (left.getMonth() > right.getMonth()) {
return 1;
} else if (left.getPrecision() == TemporalPrecisionEnum.MONTH && right.getPrecision() == TemporalPrecisionEnum.MONTH) {
return 0;
} else if (left.getPrecision() == TemporalPrecisionEnum.MONTH || right.getPrecision() == TemporalPrecisionEnum.MONTH) {
return def;
}
if (left.getDay() < right.getDay()) {
return -1;
} else if (left.getDay() > right.getDay()) {
return 1;
} else if (left.getPrecision() == TemporalPrecisionEnum.DAY && right.getPrecision() == TemporalPrecisionEnum.DAY) {
return 0;
} else if (left.getPrecision() == TemporalPrecisionEnum.DAY || right.getPrecision() == TemporalPrecisionEnum.DAY) {
return def;
}
if (left.getHour() < right.getHour()) {
return -1;
} else if (left.getHour() > right.getHour()) {
return 1;
// hour is not a valid precision
// } else if (dateLeft.getPrecision() == TemporalPrecisionEnum.YEAR && dateRight.getPrecision() == TemporalPrecisionEnum.YEAR) {
// return 0;
// } else if (dateLeft.getPrecision() == TemporalPrecisionEnum.HOUR || dateRight.getPrecision() == TemporalPrecisionEnum.HOUR) {
// return null;
}
if (left.getMinute() < right.getMinute()) {
return -1;
} else if (left.getMinute() > right.getMinute()) {
return 1;
} else if (left.getPrecision() == TemporalPrecisionEnum.MINUTE && right.getPrecision() == TemporalPrecisionEnum.MINUTE) {
return 0;
} else if (left.getPrecision() == TemporalPrecisionEnum.MINUTE || right.getPrecision() == TemporalPrecisionEnum.MINUTE) {
return def;
}
if (left.getSecond() < right.getSecond()) {
return -1;
} else if (left.getSecond() > right.getSecond()) {
return 1;
} else if (left.getPrecision() == TemporalPrecisionEnum.SECOND && right.getPrecision() == TemporalPrecisionEnum.SECOND) {
return 0;
}
if (left.getSecondsMilli() < right.getSecondsMilli()) {
return -1;
} else if (left.getSecondsMilli() > right.getSecondsMilli()) {
return 1;
} else {
return 0;
}
}
@Override
public String fpValue() {
return "@"+primitiveValue();
}
private TimeZone getTimeZone(String offset) {
return timezoneCache.computeIfAbsent(offset, TimeZone::getTimeZone);

View File

@ -46,10 +46,16 @@ public class ExpressionNode {
Custom,
Empty, Not, Exists, SubsetOf, SupersetOf, IsDistinct, Distinct, Count, Where, Select, All, Repeat, Aggregate, Item /*implicit from name[]*/, As, Is, Single,
First, Last, Tail, Skip, Take, Union, Combine, Intersect, Exclude, Iif, Upper, Lower, ToChars, IndexOf, Substring, StartsWith, EndsWith, Matches, ReplaceMatches, Contains, Replace, Length,
First, Last, Tail, Skip, Take, Union, Combine, Intersect, Exclude, Iif, Upper, Lower, ToChars, IndexOf, Substring, StartsWith, EndsWith, Matches, MatchesFull, ReplaceMatches, Contains, Replace, Length,
Children, Descendants, MemberOf, Trace, Check, Today, Now, Resolve, Extension, AllFalse, AnyFalse, AllTrue, AnyTrue,
HasValue, AliasAs, Alias, HtmlChecks, OfType, Type,
ConvertsToBoolean, ConvertsToInteger, ConvertsToString, ConvertsToDecimal, ConvertsToQuantity, ConvertsToDateTime, ConvertsToTime, ToBoolean, ToInteger, ToString, ToDecimal, ToQuantity, ToDateTime, ToTime, ConformsTo;
HasValue, OfType, Type, ConvertsToBoolean, ConvertsToInteger, ConvertsToString, ConvertsToDecimal, ConvertsToQuantity, ConvertsToDateTime, ConvertsToDate, ConvertsToTime, ToBoolean, ToInteger, ToString, ToDecimal, ToQuantity, ToDateTime, ToTime, ConformsTo,
Round, Sqrt, Abs, Ceiling, Exp, Floor, Ln, Log, Power, Truncate,
// R3 functions
Encode, Decode, Escape, Unescape, Trim, Split, Join, LowBoundary, HighBoundary, Precision,
// Local extensions to FHIRPath
HtmlChecks1, HtmlChecks2, AliasAs, Alias;
public static Function fromCode(String name) {
if (name.equals("empty")) return Function.Empty;
@ -87,6 +93,7 @@ public class ExpressionNode {
if (name.equals("startsWith")) return Function.StartsWith;
if (name.equals("endsWith")) return Function.EndsWith;
if (name.equals("matches")) return Function.Matches;
if (name.equals("matchesFull")) return Function.MatchesFull;
if (name.equals("replaceMatches")) return Function.ReplaceMatches;
if (name.equals("contains")) return Function.Contains;
if (name.equals("replace")) return Function.Replace;
@ -107,7 +114,16 @@ public class ExpressionNode {
if (name.equals("hasValue")) return Function.HasValue;
if (name.equals("alias")) return Function.Alias;
if (name.equals("aliasAs")) return Function.AliasAs;
if (name.equals("htmlChecks")) return Function.HtmlChecks;
if (name.equals("htmlChecks")) return Function.HtmlChecks1;
if (name.equals("htmlchecks")) return Function.HtmlChecks1; // support change of care from R3
if (name.equals("htmlChecks2")) return Function.HtmlChecks2;
if (name.equals("encode")) return Function.Encode;
if (name.equals("decode")) return Function.Decode;
if (name.equals("escape")) return Function.Escape;
if (name.equals("unescape")) return Function.Unescape;
if (name.equals("trim")) return Function.Trim;
if (name.equals("split")) return Function.Split;
if (name.equals("join")) return Function.Join;
if (name.equals("ofType")) return Function.OfType;
if (name.equals("type")) return Function.Type;
if (name.equals("toInteger")) return Function.ToInteger;
@ -123,8 +139,23 @@ public class ExpressionNode {
if (name.equals("convertsToQuantity")) return Function.ConvertsToQuantity;
if (name.equals("convertsToBoolean")) return Function.ConvertsToBoolean;
if (name.equals("convertsToDateTime")) return Function.ConvertsToDateTime;
if (name.equals("convertsToDate")) return Function.ConvertsToDate;
if (name.equals("convertsToTime")) return Function.ConvertsToTime;
if (name.equals("conformsTo")) return Function.ConformsTo;
if (name.equals("round")) return Function.Round;
if (name.equals("sqrt")) return Function.Sqrt;
if (name.equals("abs")) return Function.Abs;
if (name.equals("ceiling")) return Function.Ceiling;
if (name.equals("exp")) return Function.Exp;
if (name.equals("floor")) return Function.Floor;
if (name.equals("ln")) return Function.Ln;
if (name.equals("log")) return Function.Log;
if (name.equals("power")) return Function.Power;
if (name.equals("truncate")) return Function.Truncate;
if (name.equals("lowBoundary")) return Function.LowBoundary;
if (name.equals("highBoundary")) return Function.HighBoundary;
if (name.equals("precision")) return Function.Precision;
return null;
}
public String toCode() {
@ -164,6 +195,7 @@ public class ExpressionNode {
case StartsWith : return "startsWith";
case EndsWith : return "endsWith";
case Matches : return "matches";
case MatchesFull : return "matchesFull";
case ReplaceMatches : return "replaceMatches";
case Contains : return "contains";
case Replace : return "replace";
@ -184,7 +216,15 @@ public class ExpressionNode {
case HasValue : return "hasValue";
case Alias : return "alias";
case AliasAs : return "aliasAs";
case HtmlChecks : return "htmlChecks";
case Encode : return "encode";
case Decode : return "decode";
case Escape : return "escape";
case Unescape : return "unescape";
case Trim : return "trim";
case Split : return "split";
case Join : return "join";
case HtmlChecks1 : return "htmlChecks";
case HtmlChecks2 : return "htmlChecks2";
case OfType : return "ofType";
case Type : return "type";
case ToInteger : return "toInteger";
@ -200,9 +240,23 @@ public class ExpressionNode {
case ConvertsToBoolean : return "convertsToBoolean";
case ConvertsToQuantity : return "convertsToQuantity";
case ConvertsToDateTime : return "convertsToDateTime";
case ConvertsToDate : return "convertsToDate";
case ConvertsToTime : return "isTime";
case ConformsTo : return "conformsTo";
default: return "??";
case Round : return "round";
case Sqrt : return "sqrt";
case Abs : return "abs";
case Ceiling : return "ceiling";
case Exp : return "exp";
case Floor : return "floor";
case Ln : return "ln";
case Log : return "log";
case Power : return "power";
case Truncate: return "truncate";
case LowBoundary: return "lowBoundary";
case HighBoundary: return "highBoundary";
case Precision: return "precision";
default: return "?custom?";
}
}
}
@ -294,7 +348,7 @@ public class ExpressionNode {
case In : return "in";
case Contains : return "contains";
case MemberOf : return "memberOf";
default: return "??";
default: return "?custom?";
}
}
}
@ -446,7 +500,7 @@ public class ExpressionNode {
if (!name.startsWith("$"))
return true;
else
return Utilities.existsInList(name, "$this", "$total");
return Utilities.existsInList(name, "$this", "$total", "$index");
}
public Kind getKind() {
@ -522,7 +576,7 @@ public class ExpressionNode {
case Constant: return uniqueId+": "+constant;
case Group: return uniqueId+": (Group)";
}
return "??";
return "?exp-kind?";
}
private void write(StringBuilder b) {
@ -568,6 +622,9 @@ public class ExpressionNode {
public String check() {
if (kind == null) {
return "Error in expression - node has no kind";
}
switch (kind) {
case Name:
if (Utilities.noString(name))

View File

@ -34,6 +34,8 @@ package org.hl7.fhir.r4.model;
import java.util.ArrayList;
import java.util.List;
import org.hl7.fhir.utilities.MergedList.IMatcher;
/**
* A child element or property defined by the FHIR specification
* This class is defined as a helper class when iterating the
@ -47,6 +49,16 @@ import java.util.List;
*/
public class Property {
public static class PropertyMatcher implements IMatcher<Property> {
@Override
public boolean match(Property l, Property r) {
return l.getName().equals(r.getName());
}
}
/**
* The name of the property as found in the FHIR specification
*/

View File

@ -1,5 +1,9 @@
package org.hl7.fhir.r4.model;
import org.hl7.fhir.utilities.Utilities;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
/*
Copyright (c) 2011+, HL7, Inc.
All rights reserved.
@ -77,4 +81,71 @@ public class TimeType extends PrimitiveType<String> {
return "time";
}
public int getHour() {
String v = getValue();
if (v.length() < 2) {
return 0;
}
v = v.substring(0, 2);
if (!Utilities.isInteger(v)) {
return 0;
}
return Integer.parseInt(v);
}
public int getMinute() {
String v = getValue();
if (v.length() < 5) {
return 0;
}
v = v.substring(3, 5);
if (!Utilities.isInteger(v)) {
return 0;
}
return Integer.parseInt(v);
}
public float getSecond() {
String v = getValue();
if (v.length() < 8) {
return 0;
}
v = v.substring(6);
if (!Utilities.isDecimal(v, false, true)) {
return 0;
}
return Float.parseFloat(v);
}
public TemporalPrecisionEnum getPrecision() {
String v = getValue();
// if (v.length() == 2) {
// return TemporalPrecisionEnum.HOUR;
// }
if (v.length() == 5) {
return TemporalPrecisionEnum.MINUTE;
}
if (v.length() == 8) {
return TemporalPrecisionEnum.SECOND;
}
if (v.length() > 9) {
return TemporalPrecisionEnum.MILLI;
}
return null;
}
public void setPrecision(TemporalPrecisionEnum temp) {
if (temp == TemporalPrecisionEnum.MINUTE) {
setValue(getValue().substring(0, 5));
}
if (temp == TemporalPrecisionEnum.SECOND) {
setValue(getValue().substring(0, 8));
}
}
@Override
public String fpValue() {
return "@T"+primitiveValue();
}
}

View File

@ -32,6 +32,7 @@ package org.hl7.fhir.r4.model;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
@ -57,6 +58,7 @@ public class TypeDetails {
public static final String FP_Time = "http://hl7.org/fhirpath/Time";
public static final String FP_SimpleTypeInfo = "http://hl7.org/fhirpath/SimpleTypeInfo";
public static final String FP_ClassInfo = "http://hl7.org/fhirpath/ClassInfo";
public static final Set<String> FP_NUMBERS = new HashSet<String>(Arrays.asList(FP_Integer, FP_Decimal));
public static class ProfiledType {
private String uri;
@ -207,7 +209,7 @@ public class TypeDetails {
if (tail != null && typesContains(sd.getUrl()+"#"+sd.getType()+tail))
return true;
if (sd.hasBaseDefinition()) {
if (sd.getBaseDefinition().equals("http://hl7.org/fhir/StructureDefinition/Element") && !sd.getType().equals("string") && sd.getType().equals("uri"))
if (sd.getType().equals("uri"))
sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/string");
else
sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
@ -296,7 +298,7 @@ public class TypeDetails {
String t = ProfiledType.ns(n);
if (typesContains(t))
return true;
if (Utilities.existsInList(n, "boolean", "string", "integer", "decimal", "Quantity", "dateTime", "time", "ClassInfo", "SimpleTypeInfo")) {
if (Utilities.existsInList(n, "boolean", "string", "integer", "decimal", "Quantity", "date", "dateTime", "time", "ClassInfo", "SimpleTypeInfo")) {
t = FP_NS+Utilities.capitalize(n);
if (typesContains(t))
return true;

View File

@ -46,17 +46,20 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.fhir.ucum.UcumEssenceService;
import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.context.SimpleWorkerContext;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.utilities.CSFile;
import org.hl7.fhir.utilities.TextFile;
import org.hl7.fhir.utilities.ToolGlobalSettings;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.hl7.fhir.utilities.npm.ToolsVersion;
import org.hl7.fhir.utilities.tests.BaseTestingUtilities;
import org.hl7.fhir.utilities.tests.ResourceLoaderTests;
import org.hl7.fhir.utilities.tests.TestConfig;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
@ -476,6 +479,38 @@ public class TestingUtilities {
ResourceLoaderTests.copyResourceToFile(TestingUtilities.class, newFilePath, resourcePath);
}
public static String loadTestResource(String... paths) throws IOException {
/**
* This 'if' condition checks to see if the fhir-test-cases project (https://github.com/FHIR/fhir-test-cases) is
* installed locally at the same directory level as the core library project is. If so, the test case data is read
* directly from that project, instead of the imported maven dependency jar. It is important, that if you want to
* test against the dependency imported from sonatype nexus, instead of your local copy, you need to either change
* the name of the project directory to something other than 'fhir-test-cases', or move it to another location, not
* at the same directory level as the core project.
*/
String dir = TestConfig.getInstance().getFhirTestCasesDirectory();
if (dir == null && ToolGlobalSettings.hasTestsPath()) {
dir = ToolGlobalSettings.getTestsPath();
}
if (dir != null && new CSFile(dir).exists()) {
String n = Utilities.path(dir, Utilities.path(paths));
// ok, we'll resolve this locally
return TextFile.fileToString(new CSFile(n));
} else {
// resolve from the package
String contents;
String classpath = ("/org/hl7/fhir/testcases/" + Utilities.pathURL(paths));
try (InputStream inputStream = BaseTestingUtilities.class.getResourceAsStream(classpath)) {
if (inputStream == null) {
throw new IOException("Can't find file on classpath: " + classpath);
}
contents = IOUtils.toString(inputStream, java.nio.charset.StandardCharsets.UTF_8);
}
return contents;
}
}
}

View File

@ -0,0 +1,20 @@
package org.hl7.fhir.r4.utils;
import org.hl7.fhir.utilities.Utilities;
public class FHIRPathConstant {
public static boolean isFHIRPathConstant(String string) {
return !Utilities.noString(string) && ((string.charAt(0) == '\'' || string.charAt(0) == '"') || string.charAt(0) == '@' || string.charAt(0) == '%' ||
string.charAt(0) == '-' || string.charAt(0) == '+' || (string.charAt(0) >= '0' && string.charAt(0) <= '9') ||
string.equals("true") || string.equals("false") || string.equals("{}"));
}
public static boolean isFHIRPathFixedName(String string) {
return string != null && (string.charAt(0) == '`');
}
public static boolean isFHIRPathStringConstant(String string) {
return string.charAt(0) == '\'' || string.charAt(0) == '"' || string.charAt(0) == '`';
}
}

View File

@ -410,11 +410,11 @@ public class LiquidEngine implements IEvaluationContext {
}
@Override
public Base resolveReference(Object appContext, String url) throws FHIRException {
public Base resolveReference(Object appContext, String url, Base base) throws FHIRException {
if (externalHostServices == null)
return null;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return resolveReference(ctxt.externalContext, url);
return resolveReference(ctxt.externalContext, url, base);
}
@Override

View File

@ -212,7 +212,7 @@ public class StructureMapUtilities {
}
@Override
public Base resolveReference(Object appContext, String url) throws FHIRException {
public Base resolveReference(Object appContext, String url, Base base) throws FHIRException {
if (services == null)
return null;
return services.resolveReference(appContext, url);

View File

@ -1,9 +1,17 @@
package org.hl7.fhir.r4.test;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.lang3.NotImplementedException;
import org.fhir.ucum.UcumException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.PathEngineException;
import org.hl7.fhir.r4.formats.JsonParser;
import org.hl7.fhir.r4.formats.XmlParser;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.test.utils.TestingUtilities;
@ -11,7 +19,10 @@ import org.hl7.fhir.r4.utils.FHIRPathEngine;
import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.xml.XMLUtil;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@ -20,18 +31,14 @@ import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class FHIRPathTests {
public enum TestResultType {OK, SYNTAX, SEMANTICS, EXECUTION}
public class FHIRPathTestEvaluationServices implements IEvaluationContext {
@Override
@ -41,7 +48,6 @@ public class FHIRPathTests {
@Override
public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
throw new NotImplementedException("Not done yet (FHIRPathTestEvaluationServices.resolveConstantType), when item is element");
}
@ -66,7 +72,7 @@ public class FHIRPathTests {
}
@Override
public Base resolveReference(Object appContext, String url) throws FHIRException {
public Base resolveReference(Object appContext, String url, Base base) throws FHIRException {
throw new NotImplementedException("Not done yet (FHIRPathTestEvaluationServices.resolveReference), when item is element");
}
@ -84,29 +90,33 @@ public class FHIRPathTests {
public ValueSet resolveValueSet(Object appContext, String url) {
return TestingUtilities.context().fetchResource(ValueSet.class, url);
}
}
private static FHIRPathEngine fp;
private final Map<String, Resource> resources = new HashMap<String, Resource>();
@BeforeAll
public static void setup() {
public static void setUp() {
fp = new FHIRPathEngine(TestingUtilities.context());
}
public static Stream<Arguments> data() throws ParserConfigurationException, SAXException, IOException {
Document dom = XMLUtil.parseFileToDom(TestingUtilities.resourceNameToFile("fhirpath", "tests-fhir-r4.xml"));
Document dom = XMLUtil.parseToDom(TestingUtilities.loadTestResource("r4", "fhirpath", "tests-fhir-r4.xml"));
List<Element> list = new ArrayList<Element>();
List<Element> groups = new ArrayList<Element>();
XMLUtil.getNamedChildren(dom.getDocumentElement(), "group", groups);
for (Element g : groups) {
XMLUtil.getNamedChildren(g, "test", list);
XMLUtil.getNamedChildren(g, "modeTest", list);
}
List<Arguments> objects = new ArrayList();
List<Arguments> objects = new ArrayList<>();
for (Element e : list) {
objects.add(Arguments.of(getName(e), e));
}
return objects.stream();
}
@ -116,95 +126,144 @@ public class FHIRPathTests {
int ndx = 0;
for (int i = 0; i < p.getChildNodes().getLength(); i++) {
Node c = p.getChildNodes().item(i);
if (c == e)
if (c == e) {
break;
else if (c instanceof Element)
} else if (c instanceof Element) {
ndx++;
}
}
if (Utilities.noString(s))
if (Utilities.noString(s)) {
s = "?? - G " + p.getAttribute("name") + "[" + Integer.toString(ndx + 1) + "]";
else
} else {
s = s + " - G " + p.getAttribute("name") + "[" + Integer.toString(ndx + 1) + "]";
}
return s;
}
private Map<String, Resource> resources = new HashMap<String, Resource>();
@SuppressWarnings("deprecation")
@Disabled
@ParameterizedTest(name = "{index}: file {0}")
@MethodSource("data")
public void test(String name, Element test) throws FileNotFoundException, IOException, FHIRException, org.hl7.fhir.exceptions.FHIRException, UcumException {
// Setting timezone for this test. Grahame is in UTC+11, Travis is in GMT, and I'm here in Toronto, Canada with
// all my time based tests failing locally...
TimeZone.setDefault(TimeZone.getTimeZone("UTC+1100"));
fp.setHostServices(new FHIRPathTestEvaluationServices());
String input = test.getAttribute("inputfile");
String expression = XMLUtil.getNamedChild(test, "expression").getTextContent();
boolean fail = Utilities.existsInList(XMLUtil.getNamedChild(test, "expression").getAttribute("invalid"), "true", "semantic");
TestResultType fail = TestResultType.OK;
if ("syntax".equals(XMLUtil.getNamedChild(test, "expression").getAttribute("invalid"))) {
fail = TestResultType.SYNTAX;
} else if ("semantic".equals(XMLUtil.getNamedChild(test, "expression").getAttribute("invalid"))) {
fail = TestResultType.SEMANTICS;
} else if ("execution".equals(XMLUtil.getNamedChild(test, "expression").getAttribute("invalid"))) {
fail = TestResultType.EXECUTION;
};
Resource res = null;
List<Base> outcome = new ArrayList<Base>();
ExpressionNode node = fp.parse(expression);
System.out.println(name);
ExpressionNode node = null;
try {
if (Utilities.noString(input))
fp.check(null, null, node);
else {
node = fp.parse(expression);
Assertions.assertTrue(fail != TestResultType.SYNTAX, String.format("Expected exception didn't occur parsing %s", expression));
} catch (Exception e) {
System.out.println("Parsing Error: "+e.getMessage());
Assertions.assertTrue(fail == TestResultType.SYNTAX, String.format("Unexpected exception parsing %s: " + e.getMessage(), expression));
}
if (node != null) {
if (!Utilities.noString(input)) {
res = resources.get(input);
if (res == null) {
res = new XmlParser().parse(new FileInputStream(TestingUtilities.resourceNameToFile(input)));
resources.put(input, res);
}
fp.check(res, res.getResourceType().toString(), res.getResourceType().toString(), node);
}
outcome = fp.evaluate(res, node);
Assertions.assertFalse(fail, String.format("Expected exception parsing %s", expression));
} catch (Exception e) {
Assertions.assertTrue(fail, String.format("Unexpected exception parsing %s: " + e.getMessage(), expression));
}
if ("true".equals(test.getAttribute("predicate"))) {
boolean ok = fp.convertToBoolean(outcome);
outcome.clear();
outcome.add(new BooleanType(ok));
}
if (fp.hasLog())
System.out.println(fp.takeLog());
List<Element> expected = new ArrayList<Element>();
XMLUtil.getNamedChildren(test, "output", expected);
Assertions.assertEquals(outcome.size(), expected.size(), String.format("Expected %d objects but found %d for expression %s", expected.size(), outcome.size(), expression));
if ("false".equals(test.getAttribute("ordered"))) {
for (int i = 0; i < Math.min(outcome.size(), expected.size()); i++) {
String tn = outcome.get(i).fhirType();
String s;
if (outcome.get(i) instanceof Quantity)
s = fp.convertToString(outcome.get(i));
else
s = ((PrimitiveType) outcome.get(i)).asStringValue();
boolean found = false;
for (Element e : expected) {
if ((Utilities.noString(e.getAttribute("type")) || e.getAttribute("type").equals(tn)) &&
(Utilities.noString(e.getTextContent()) || e.getTextContent().equals(s)))
found = true;
}
Assertions.assertTrue(found, String.format("Outcome %d: Value %s of type %s not expected for %s", i, s, tn, expression));
}
} else {
for (int i = 0; i < Math.min(outcome.size(), expected.size()); i++) {
String tn = expected.get(i).getAttribute("type");
if (!Utilities.noString(tn)) {
Assertions.assertEquals(tn, outcome.get(i).fhirType(), String.format("Outcome %d: Type should be %s but was %s", i, tn, outcome.get(i).fhirType()));
}
String v = expected.get(i).getTextContent();
if (!Utilities.noString(v)) {
if (outcome.get(i) instanceof Quantity) {
Quantity q = fp.parseQuantityString(v);
Assertions.assertTrue(outcome.get(i).equalsDeep(q), String.format("Outcome %d: Value should be %s but was %s", i, v, outcome.get(i).toString()));
if (input.endsWith(".json")) {
res = new JsonParser().parse(TestingUtilities.loadTestResource("r4", input));
} else {
Assertions.assertTrue(outcome.get(i) instanceof PrimitiveType, String.format("Outcome %d: Value should be a primitive type but was %s", i, outcome.get(i).fhirType()));
Assertions.assertEquals(v, ((PrimitiveType) outcome.get(i)).fpValue(), String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, ((PrimitiveType) outcome.get(i)).fpValue(), expression));
res = new XmlParser().parse(TestingUtilities.loadTestResource("r4", input));
}
resources.put(input, res);
}
}
try {
if (Utilities.noString(input)) {
fp.check(null, null, node);
} else {
fp.check(res, res.getResourceType().toString(), res.getResourceType().toString(), node);
}
Assertions.assertTrue(fail != TestResultType.SEMANTICS, String.format("Expected exception didn't occur checking %s", expression));
} catch (Exception e) {
System.out.println("Checking Error: "+e.getMessage());
Assertions.assertTrue(fail == TestResultType.SEMANTICS, String.format("Unexpected exception checking %s: " + e.getMessage(), expression));
node = null;
}
}
if (node != null) {
try {
outcome = fp.evaluate(res, node);
Assertions.assertTrue(fail == TestResultType.OK, String.format("Expected exception didn't occur executing %s", expression));
} catch (Exception e) {
System.out.println("Execution Error: "+e.getMessage());
Assertions.assertTrue(fail == TestResultType.EXECUTION, String.format("Unexpected exception executing %s: " + e.getMessage(), expression));
node = null;
}
}
if (fp.hasLog()) {
System.out.println(name);
System.out.println(fp.takeLog());
}
if (node != null) {
if ("true".equals(test.getAttribute("predicate"))) {
boolean ok = fp.convertToBoolean(outcome);
outcome.clear();
outcome.add(new BooleanType(ok));
}
List<Element> expected = new ArrayList<Element>();
XMLUtil.getNamedChildren(test, "output", expected);
assertEquals(outcome.size(), expected.size(), String.format("Expected %d objects but found %d for expression %s", expected.size(), outcome.size(), expression));
if ("false".equals(test.getAttribute("ordered"))) {
for (int i = 0; i < Math.min(outcome.size(), expected.size()); i++) {
String tn = outcome.get(i).fhirType();
String s;
if (outcome.get(i) instanceof Quantity) {
s = fp.convertToString(outcome.get(i));
} else {
s = ((PrimitiveType) outcome.get(i)).asStringValue();
}
boolean found = false;
for (Element e : expected) {
if ((Utilities.noString(e.getAttribute("type")) || e.getAttribute("type").equals(tn)) &&
(Utilities.noString(e.getTextContent()) || e.getTextContent().equals(s))) {
found = true;
}
}
Assertions.assertTrue(found, String.format("Outcome %d: Value %s of type %s not expected for %s", i, s, tn, expression));
}
} else {
for (int i = 0; i < Math.min(outcome.size(), expected.size()); i++) {
String tn = expected.get(i).getAttribute("type");
if (!Utilities.noString(tn)) {
assertEquals(tn, outcome.get(i).fhirType(), String.format("Outcome %d: Type should be %s but was %s", i, tn, outcome.get(i).fhirType()));
}
String v = expected.get(i).getTextContent();
if (!Utilities.noString(v)) {
if (outcome.get(i) instanceof Quantity) {
Quantity q = fp.parseQuantityString(v);
Assertions.assertTrue(outcome.get(i).equalsDeep(q), String.format("Outcome %d: Value should be %s but was %s", i, v, outcome.get(i).toString()));
} else {
Assertions.assertTrue(outcome.get(i) instanceof PrimitiveType, String.format("Outcome %d: Value should be a primitive type but was %s", i, outcome.get(i).fhirType()));
if (!(v.equals(((PrimitiveType) outcome.get(i)).fpValue()))) {
System.out.println(name);
System.out.println(String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, ((PrimitiveType) outcome.get(i)).fpValue(), expression));
}
assertEquals(v, ((PrimitiveType) outcome.get(i)).fpValue(), String.format("Outcome %d: Value should be %s but was %s for expression %s", i, v, ((PrimitiveType) outcome.get(i)).fpValue(), expression));
}
}
}
}

View File

@ -331,7 +331,7 @@ public class SnapShotGenerationTests {
}
@Override
public Base resolveReference(Object appContext, String url) {
public Base resolveReference(Object appContext, String url, Base base) {
// TODO Auto-generated method stub
return null;
}