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; 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 org.apache.commons.lang3.time.DateUtils;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import org.hl7.fhir.utilities.DateTimeUtil; import org.hl7.fhir.utilities.DateTimeUtil;
import org.hl7.fhir.utilities.Utilities;
public abstract class BaseDateTimeType extends PrimitiveType<Date> { public abstract class BaseDateTimeType extends PrimitiveType<Date> {
@ -286,6 +288,13 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
return getFieldValue(Calendar.MONTH); 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 * Returns the nanoseconds within the current second
* <p> * <p>
@ -867,84 +876,157 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
* </ul> * </ul>
*/ */
public Boolean equalsUsingFhirPathRules(BaseDateTimeType theOther) { public Boolean equalsUsingFhirPathRules(BaseDateTimeType theOther) {
if (hasTimezone() != theOther.hasTimezone()) {
BaseDateTimeType me = this; if (!couldBeTheSameTime(this, theOther)) {
return false;
// 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;
} else { } else {
String leftTime = me.getValueAsString(); return null;
String rightTime = theOther.getValueAsString();
return leftTime.equals(rightTime);
} }
} } else {
BaseDateTimeType left = (BaseDateTimeType) this.copy();
// Both represent 0 millis but the millis are optional BaseDateTimeType right = (BaseDateTimeType) theOther.copy();
if (((Integer)0).equals(me.getMillis())) { if (left.hasTimezone() && left.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
if (((Integer)0).equals(theOther.getMillis())) { left.setTimeZoneZulu(true);
if (me.getPrecision().ordinal() >= TemporalPrecisionEnum.SECOND.ordinal()) {
if (theOther.getPrecision().ordinal() >= TemporalPrecisionEnum.SECOND.ordinal()) {
return me.getValue().getTime() == theOther.getValue().getTime();
}
}
} }
} if (right.hasTimezone() && right.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
right.setTimeZoneZulu(true);
return false; }
Integer i = compareTimes(left, right, null);
return i == null ? null : i == 0;
}
} }
private boolean couldBeTheSameTime(BaseDateTimeType theArg1, BaseDateTimeType theArg2) { private boolean couldBeTheSameTime(BaseDateTimeType theArg1, BaseDateTimeType theArg2) {
boolean theCouldBeTheSameTime = false; long lowLeft = theArg1.getValue().getTime();
if (theArg1.getTimeZone() == null && theArg2.getTimeZone() != null) { long highLeft = theArg1.getHighEdge().getValue().getTime();
long lowLeft = new DateTimeType(theArg1.getValueAsString()+"Z").getValue().getTime() - (14 * DateUtils.MILLIS_PER_HOUR); if (!theArg1.hasTimezone()) {
long highLeft = new DateTimeType(theArg1.getValueAsString()+"Z").getValue().getTime() + (14 * DateUtils.MILLIS_PER_HOUR); lowLeft = lowLeft - (14 * DateUtils.MILLIS_PER_HOUR);
long right = theArg2.getValue().getTime(); highLeft = highLeft + (14 * DateUtils.MILLIS_PER_HOUR);
if (right >= lowLeft && right <= highLeft) { }
theCouldBeTheSameTime = true; long lowRight = theArg2.getValue().getTime();
} long highRight = theArg2.getHighEdge().getValue().getTime();
} if (!theArg2.hasTimezone()) {
return theCouldBeTheSameTime; 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() { boolean hasTimezoneIfRequired() {
return getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal() || return getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal() ||
getTimeZone() != null; getTimeZone() != null;
} }
@Override boolean hasTimezone() {
public String fpValue() { return getTimeZone() != null;
return "@"+primitiveValue(); }
}
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) { private TimeZone getTimeZone(String offset) {
return timezoneCache.computeIfAbsent(offset, TimeZone::getTimeZone); return timezoneCache.computeIfAbsent(offset, TimeZone::getTimeZone);

View File

@ -46,10 +46,16 @@ public class ExpressionNode {
Custom, Custom,
Empty, Not, Exists, SubsetOf, SupersetOf, IsDistinct, Distinct, Count, Where, Select, All, Repeat, Aggregate, Item /*implicit from name[]*/, As, Is, Single, 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, Children, Descendants, MemberOf, Trace, Check, Today, Now, Resolve, Extension, AllFalse, AnyFalse, AllTrue, AnyTrue,
HasValue, AliasAs, Alias, HtmlChecks, OfType, Type, HasValue, OfType, Type, ConvertsToBoolean, ConvertsToInteger, ConvertsToString, ConvertsToDecimal, ConvertsToQuantity, ConvertsToDateTime, ConvertsToDate, ConvertsToTime, ToBoolean, ToInteger, ToString, ToDecimal, ToQuantity, ToDateTime, ToTime, ConformsTo,
ConvertsToBoolean, ConvertsToInteger, ConvertsToString, ConvertsToDecimal, ConvertsToQuantity, ConvertsToDateTime, 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) { public static Function fromCode(String name) {
if (name.equals("empty")) return Function.Empty; if (name.equals("empty")) return Function.Empty;
@ -87,6 +93,7 @@ public class ExpressionNode {
if (name.equals("startsWith")) return Function.StartsWith; if (name.equals("startsWith")) return Function.StartsWith;
if (name.equals("endsWith")) return Function.EndsWith; if (name.equals("endsWith")) return Function.EndsWith;
if (name.equals("matches")) return Function.Matches; if (name.equals("matches")) return Function.Matches;
if (name.equals("matchesFull")) return Function.MatchesFull;
if (name.equals("replaceMatches")) return Function.ReplaceMatches; if (name.equals("replaceMatches")) return Function.ReplaceMatches;
if (name.equals("contains")) return Function.Contains; if (name.equals("contains")) return Function.Contains;
if (name.equals("replace")) return Function.Replace; if (name.equals("replace")) return Function.Replace;
@ -107,7 +114,16 @@ public class ExpressionNode {
if (name.equals("hasValue")) return Function.HasValue; if (name.equals("hasValue")) return Function.HasValue;
if (name.equals("alias")) return Function.Alias; if (name.equals("alias")) return Function.Alias;
if (name.equals("aliasAs")) return Function.AliasAs; 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("ofType")) return Function.OfType;
if (name.equals("type")) return Function.Type; if (name.equals("type")) return Function.Type;
if (name.equals("toInteger")) return Function.ToInteger; if (name.equals("toInteger")) return Function.ToInteger;
@ -123,8 +139,23 @@ public class ExpressionNode {
if (name.equals("convertsToQuantity")) return Function.ConvertsToQuantity; if (name.equals("convertsToQuantity")) return Function.ConvertsToQuantity;
if (name.equals("convertsToBoolean")) return Function.ConvertsToBoolean; if (name.equals("convertsToBoolean")) return Function.ConvertsToBoolean;
if (name.equals("convertsToDateTime")) return Function.ConvertsToDateTime; if (name.equals("convertsToDateTime")) return Function.ConvertsToDateTime;
if (name.equals("convertsToDate")) return Function.ConvertsToDate;
if (name.equals("convertsToTime")) return Function.ConvertsToTime; if (name.equals("convertsToTime")) return Function.ConvertsToTime;
if (name.equals("conformsTo")) return Function.ConformsTo; 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; return null;
} }
public String toCode() { public String toCode() {
@ -164,6 +195,7 @@ public class ExpressionNode {
case StartsWith : return "startsWith"; case StartsWith : return "startsWith";
case EndsWith : return "endsWith"; case EndsWith : return "endsWith";
case Matches : return "matches"; case Matches : return "matches";
case MatchesFull : return "matchesFull";
case ReplaceMatches : return "replaceMatches"; case ReplaceMatches : return "replaceMatches";
case Contains : return "contains"; case Contains : return "contains";
case Replace : return "replace"; case Replace : return "replace";
@ -184,7 +216,15 @@ public class ExpressionNode {
case HasValue : return "hasValue"; case HasValue : return "hasValue";
case Alias : return "alias"; case Alias : return "alias";
case AliasAs : return "aliasAs"; 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 OfType : return "ofType";
case Type : return "type"; case Type : return "type";
case ToInteger : return "toInteger"; case ToInteger : return "toInteger";
@ -200,9 +240,23 @@ public class ExpressionNode {
case ConvertsToBoolean : return "convertsToBoolean"; case ConvertsToBoolean : return "convertsToBoolean";
case ConvertsToQuantity : return "convertsToQuantity"; case ConvertsToQuantity : return "convertsToQuantity";
case ConvertsToDateTime : return "convertsToDateTime"; case ConvertsToDateTime : return "convertsToDateTime";
case ConvertsToDate : return "convertsToDate";
case ConvertsToTime : return "isTime"; case ConvertsToTime : return "isTime";
case ConformsTo : return "conformsTo"; 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 In : return "in";
case Contains : return "contains"; case Contains : return "contains";
case MemberOf : return "memberOf"; case MemberOf : return "memberOf";
default: return "??"; default: return "?custom?";
} }
} }
} }
@ -446,7 +500,7 @@ public class ExpressionNode {
if (!name.startsWith("$")) if (!name.startsWith("$"))
return true; return true;
else else
return Utilities.existsInList(name, "$this", "$total"); return Utilities.existsInList(name, "$this", "$total", "$index");
} }
public Kind getKind() { public Kind getKind() {
@ -522,7 +576,7 @@ public class ExpressionNode {
case Constant: return uniqueId+": "+constant; case Constant: return uniqueId+": "+constant;
case Group: return uniqueId+": (Group)"; case Group: return uniqueId+": (Group)";
} }
return "??"; return "?exp-kind?";
} }
private void write(StringBuilder b) { private void write(StringBuilder b) {
@ -568,6 +622,9 @@ public class ExpressionNode {
public String check() { public String check() {
if (kind == null) {
return "Error in expression - node has no kind";
}
switch (kind) { switch (kind) {
case Name: case Name:
if (Utilities.noString(name)) if (Utilities.noString(name))

View File

@ -34,6 +34,8 @@ package org.hl7.fhir.r4.model;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.hl7.fhir.utilities.MergedList.IMatcher;
/** /**
* A child element or property defined by the FHIR specification * A child element or property defined by the FHIR specification
* This class is defined as a helper class when iterating the * This class is defined as a helper class when iterating the
@ -47,6 +49,16 @@ import java.util.List;
*/ */
public class Property { 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 * The name of the property as found in the FHIR specification
*/ */

View File

@ -1,5 +1,9 @@
package org.hl7.fhir.r4.model; package org.hl7.fhir.r4.model;
import org.hl7.fhir.utilities.Utilities;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
/* /*
Copyright (c) 2011+, HL7, Inc. Copyright (c) 2011+, HL7, Inc.
All rights reserved. All rights reserved.
@ -77,4 +81,71 @@ public class TimeType extends PrimitiveType<String> {
return "time"; 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.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; 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_Time = "http://hl7.org/fhirpath/Time";
public static final String FP_SimpleTypeInfo = "http://hl7.org/fhirpath/SimpleTypeInfo"; 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 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 { public static class ProfiledType {
private String uri; private String uri;
@ -207,7 +209,7 @@ public class TypeDetails {
if (tail != null && typesContains(sd.getUrl()+"#"+sd.getType()+tail)) if (tail != null && typesContains(sd.getUrl()+"#"+sd.getType()+tail))
return true; return true;
if (sd.hasBaseDefinition()) { 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"); sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/string");
else else
sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
@ -296,7 +298,7 @@ public class TypeDetails {
String t = ProfiledType.ns(n); String t = ProfiledType.ns(n);
if (typesContains(t)) if (typesContains(t))
return true; 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); t = FP_NS+Utilities.capitalize(n);
if (typesContains(t)) if (typesContains(t))
return true; return true;

View File

@ -46,17 +46,20 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.fhir.ucum.UcumEssenceService; import org.fhir.ucum.UcumEssenceService;
import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.context.SimpleWorkerContext; import org.hl7.fhir.r4.context.SimpleWorkerContext;
import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.utilities.CSFile; import org.hl7.fhir.utilities.CSFile;
import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.TextFile;
import org.hl7.fhir.utilities.ToolGlobalSettings;
import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.hl7.fhir.utilities.npm.ToolsVersion; import org.hl7.fhir.utilities.npm.ToolsVersion;
import org.hl7.fhir.utilities.tests.BaseTestingUtilities; import org.hl7.fhir.utilities.tests.BaseTestingUtilities;
import org.hl7.fhir.utilities.tests.ResourceLoaderTests; import org.hl7.fhir.utilities.tests.ResourceLoaderTests;
import org.hl7.fhir.utilities.tests.TestConfig;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap; import org.w3c.dom.NamedNodeMap;
@ -476,6 +479,38 @@ public class TestingUtilities {
ResourceLoaderTests.copyResourceToFile(TestingUtilities.class, newFilePath, resourcePath); 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 @Override
public Base resolveReference(Object appContext, String url) throws FHIRException { public Base resolveReference(Object appContext, String url, Base base) throws FHIRException {
if (externalHostServices == null) if (externalHostServices == null)
return null; return null;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext; LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return resolveReference(ctxt.externalContext, url); return resolveReference(ctxt.externalContext, url, base);
} }
@Override @Override

View File

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

View File

@ -1,9 +1,17 @@
package org.hl7.fhir.r4.test; 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.apache.commons.lang3.NotImplementedException;
import org.fhir.ucum.UcumException; import org.fhir.ucum.UcumException;
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.PathEngineException; 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.formats.XmlParser;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.test.utils.TestingUtilities; 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.r4.utils.FHIRPathEngine.IEvaluationContext;
import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.xml.XMLUtil; 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.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
@ -20,18 +31,14 @@ import org.w3c.dom.Element;
import org.w3c.dom.Node; import org.w3c.dom.Node;
import org.xml.sax.SAXException; 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.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class FHIRPathTests { public class FHIRPathTests {
public enum TestResultType {OK, SYNTAX, SEMANTICS, EXECUTION}
public class FHIRPathTestEvaluationServices implements IEvaluationContext { public class FHIRPathTestEvaluationServices implements IEvaluationContext {
@Override @Override
@ -41,7 +48,6 @@ public class FHIRPathTests {
@Override @Override
public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
throw new NotImplementedException("Not done yet (FHIRPathTestEvaluationServices.resolveConstantType), when item is element"); throw new NotImplementedException("Not done yet (FHIRPathTestEvaluationServices.resolveConstantType), when item is element");
} }
@ -66,7 +72,7 @@ public class FHIRPathTests {
} }
@Override @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"); 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) { public ValueSet resolveValueSet(Object appContext, String url) {
return TestingUtilities.context().fetchResource(ValueSet.class, url); return TestingUtilities.context().fetchResource(ValueSet.class, url);
} }
} }
private static FHIRPathEngine fp; private static FHIRPathEngine fp;
private final Map<String, Resource> resources = new HashMap<String, Resource>();
@BeforeAll @BeforeAll
public static void setup() { public static void setUp() {
fp = new FHIRPathEngine(TestingUtilities.context()); fp = new FHIRPathEngine(TestingUtilities.context());
} }
public static Stream<Arguments> data() throws ParserConfigurationException, SAXException, IOException { 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> list = new ArrayList<Element>();
List<Element> groups = new ArrayList<Element>(); List<Element> groups = new ArrayList<Element>();
XMLUtil.getNamedChildren(dom.getDocumentElement(), "group", groups); XMLUtil.getNamedChildren(dom.getDocumentElement(), "group", groups);
for (Element g : groups) { for (Element g : groups) {
XMLUtil.getNamedChildren(g, "test", list); XMLUtil.getNamedChildren(g, "test", list);
XMLUtil.getNamedChildren(g, "modeTest", list);
} }
List<Arguments> objects = new ArrayList(); List<Arguments> objects = new ArrayList<>();
for (Element e : list) { for (Element e : list) {
objects.add(Arguments.of(getName(e), e)); objects.add(Arguments.of(getName(e), e));
} }
return objects.stream(); return objects.stream();
} }
@ -116,95 +126,144 @@ public class FHIRPathTests {
int ndx = 0; int ndx = 0;
for (int i = 0; i < p.getChildNodes().getLength(); i++) { for (int i = 0; i < p.getChildNodes().getLength(); i++) {
Node c = p.getChildNodes().item(i); Node c = p.getChildNodes().item(i);
if (c == e) if (c == e) {
break; break;
else if (c instanceof Element) } else if (c instanceof Element) {
ndx++; ndx++;
}
} }
if (Utilities.noString(s)) if (Utilities.noString(s)) {
s = "?? - G " + p.getAttribute("name") + "[" + Integer.toString(ndx + 1) + "]"; s = "?? - G " + p.getAttribute("name") + "[" + Integer.toString(ndx + 1) + "]";
else } else {
s = s + " - G " + p.getAttribute("name") + "[" + Integer.toString(ndx + 1) + "]"; s = s + " - G " + p.getAttribute("name") + "[" + Integer.toString(ndx + 1) + "]";
}
return s; return s;
} }
private Map<String, Resource> resources = new HashMap<String, Resource>();
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@Disabled
@ParameterizedTest(name = "{index}: file {0}") @ParameterizedTest(name = "{index}: file {0}")
@MethodSource("data") @MethodSource("data")
public void test(String name, Element test) throws FileNotFoundException, IOException, FHIRException, org.hl7.fhir.exceptions.FHIRException, UcumException { 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()); fp.setHostServices(new FHIRPathTestEvaluationServices());
String input = test.getAttribute("inputfile"); String input = test.getAttribute("inputfile");
String expression = XMLUtil.getNamedChild(test, "expression").getTextContent(); 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; Resource res = null;
List<Base> outcome = new ArrayList<Base>(); List<Base> outcome = new ArrayList<Base>();
ExpressionNode node = fp.parse(expression); System.out.println(name);
ExpressionNode node = null;
try { try {
if (Utilities.noString(input)) node = fp.parse(expression);
fp.check(null, null, node); Assertions.assertTrue(fail != TestResultType.SYNTAX, String.format("Expected exception didn't occur parsing %s", expression));
else { } 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); res = resources.get(input);
if (res == null) { if (res == null) {
res = new XmlParser().parse(new FileInputStream(TestingUtilities.resourceNameToFile(input))); if (input.endsWith(".json")) {
resources.put(input, res); res = new JsonParser().parse(TestingUtilities.loadTestResource("r4", input));
}
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()));
} else { } 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())); res = new XmlParser().parse(TestingUtilities.loadTestResource("r4", input));
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)); }
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 @Override
public Base resolveReference(Object appContext, String url) { public Base resolveReference(Object appContext, String url, Base base) {
// TODO Auto-generated method stub // TODO Auto-generated method stub
return null; return null;
} }