Update R4 FHIRPath implementation from R5, and get all tests passing
This commit is contained in:
parent
8902898428
commit
6725563600
|
@ -849,4 +849,8 @@ private Map<String, Object> userData;
|
|||
return null;
|
||||
}
|
||||
|
||||
public XhtmlNode getXhtml() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,72 +876,72 @@ 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) {
|
||||
if (hasTimezone() != theOther.hasTimezone()) {
|
||||
if (!couldBeTheSameTime(this, theOther)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
String leftTime = me.getValueAsString();
|
||||
String rightTime = theOther.getValueAsString();
|
||||
return leftTime.equals(rightTime);
|
||||
BaseDateTimeType left = (BaseDateTimeType) this.copy();
|
||||
BaseDateTimeType right = (BaseDateTimeType) theOther.copy();
|
||||
if (left.hasTimezone() && left.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
|
||||
left.setTimeZoneZulu(true);
|
||||
}
|
||||
if (right.hasTimezone() && right.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
|
||||
right.setTimeZoneZulu(true);
|
||||
}
|
||||
|
||||
// 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();
|
||||
Integer i = compareTimes(left, right, null);
|
||||
return i == null ? null : i == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
return theCouldBeTheSameTime;
|
||||
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() {
|
||||
|
@ -941,6 +950,79 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
|
|||
}
|
||||
|
||||
|
||||
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();
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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) == '`';
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,86 +126,130 @@ 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 {
|
||||
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));
|
||||
node = fp.parse(expression);
|
||||
Assertions.assertTrue(fail != TestResultType.SYNTAX, String.format("Expected exception didn't occur parsing %s", expression));
|
||||
} catch (Exception e) {
|
||||
Assertions.assertTrue(fail, String.format("Unexpected exception parsing %s: " + e.getMessage(), expression));
|
||||
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) {
|
||||
if (input.endsWith(".json")) {
|
||||
res = new JsonParser().parse(TestingUtilities.loadTestResource("r4", input));
|
||||
} else {
|
||||
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));
|
||||
}
|
||||
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));
|
||||
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)
|
||||
if (outcome.get(i) instanceof Quantity) {
|
||||
s = fp.convertToString(outcome.get(i));
|
||||
else
|
||||
} 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)))
|
||||
(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()));
|
||||
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)) {
|
||||
|
@ -204,7 +258,12 @@ public class FHIRPathTests {
|
|||
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()));
|
||||
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));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue