Improvements to client and SP indexing in JPA server

This commit is contained in:
James Agnew 2018-07-23 17:37:49 +09:00
parent 519590ee27
commit 23083a9283
30 changed files with 3159 additions and 2609 deletions

View File

@ -20,16 +20,23 @@ package ca.uhn.fhir.rest.gclient;
* #L%
*/
import java.util.Date;
import ca.uhn.fhir.rest.param.DateRangeParam;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
public interface IHistoryTyped<T> extends IClientExecutable<IHistoryTyped<T>, T> {
import java.util.Date;
public interface IHistoryTyped<T> extends IClientExecutable<IHistoryTyped<T>, T> {
/**
* Request that the server return only resource versions that were created at or after the given time (inclusive)
* Request that the server return only the history elements between the
* specific range
*/
IHistoryTyped<T> since(Date theCutoff);
IHistoryTyped<T> at(DateRangeParam theDateRangeParam);
/**
* Request that the server return only up to <code>theCount</code> number of resources
*/
IHistoryTyped<T> count(Integer theCount);
/**
* Request that the server return only resource versions that were created at or after the given time (inclusive)
@ -41,9 +48,9 @@ public interface IHistoryTyped<T> extends IClientExecutable<IHistoryTyped<T>, T>
IHistoryTyped<T> since(IPrimitiveType<Date> theCutoff);
/**
* Request that the server return only up to <code>theCount</code> number of resources
* Request that the server return only resource versions that were created at or after the given time (inclusive)
*/
IHistoryTyped<T> count(Integer theCount);
IHistoryTyped<T> since(Date theCutoff);
}

View File

@ -1,8 +1,18 @@
package ca.uhn.fhir.rest.param;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.EQUAL;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import static ca.uhn.fhir.rest.param.ParamPrefixEnum.*;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -25,15 +35,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* limitations under the License.
* #L%
*/
import java.util.*;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class DateRangeParam implements IQueryParameterAnd<DateParam> {
@ -53,14 +54,12 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
/**
* Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public DateRangeParam(Date theLowerBound, Date theUpperBound) {
this();
@ -84,22 +83,22 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
} else {
switch (theDateParam.getPrefix()) {
case EQUAL:
setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
break;
case STARTS_AFTER:
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
validateAndSet(theDateParam, null);
break;
case ENDS_BEFORE:
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
validateAndSet(null, theDateParam);
break;
default:
// Should not happen
throw new InvalidRequestException("Invalid comparator for date range parameter:" + theDateParam.getPrefix() + ". This is a bug.");
case EQUAL:
setRangeFromDatesInclusive(theDateParam.getValueAsString(), theDateParam.getValueAsString());
break;
case STARTS_AFTER:
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
validateAndSet(theDateParam, null);
break;
case ENDS_BEFORE:
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
validateAndSet(null, theDateParam);
break;
default:
// Should not happen
throw new InvalidRequestException("Invalid comparator for date range parameter:" + theDateParam.getPrefix() + ". This is a bug.");
}
}
}
@ -107,14 +106,12 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
/**
* Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public DateRangeParam(DateParam theLowerBound, DateParam theUpperBound) {
this();
@ -124,14 +121,12 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
/**
* Constructor which takes two Dates representing the lower and upper bounds of the range (inclusive on both ends)
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public DateRangeParam(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
this();
@ -141,14 +136,12 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
/**
* Constructor which takes two strings representing the lower and upper bounds of the range (inclusive on both ends)
*
* @param theLowerBound
* An unqualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
* one may be null, but it is not valid for both to be null.
* @param theUpperBound
* An unqualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
* one may be null, but it is not valid for both to be null.
* @param theLowerBound An unqualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
* one may be null, but it is not valid for both to be null.
* @param theUpperBound An unqualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Either theLowerBound or theUpperBound may both be populated, or
* one may be null, but it is not valid for both to be null.
*/
public DateRangeParam(String theLowerBound, String theUpperBound) {
this();
@ -172,31 +165,49 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
} else {
switch (theParsed.getPrefix()) {
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
if (myLowerBound != null) {
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound");
}
myLowerBound = theParsed;
break;
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
if (myUpperBound != null) {
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound");
}
myUpperBound = theParsed;
break;
default:
throw new InvalidRequestException("Unknown comparator: " + theParsed.getPrefix());
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
if (myLowerBound != null) {
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify a lower bound");
}
myLowerBound = theParsed;
break;
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
if (myUpperBound != null) {
throw new InvalidRequestException("Can not have multiple date range parameters for the same param that specify an upper bound");
}
myUpperBound = theParsed;
break;
default:
throw new InvalidRequestException("Unknown comparator: " + theParsed.getPrefix());
}
}
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof DateRangeParam)) {
return false;
}
DateRangeParam other = (DateRangeParam) obj;
return Objects.equals(myLowerBound, other.myLowerBound) &&
Objects.equals(myUpperBound, other.myUpperBound);
}
public DateParam getLowerBound() {
return myLowerBound;
}
public DateRangeParam setLowerBound(DateParam theLowerBound) {
validateAndSet(theLowerBound, myUpperBound);
return this;
}
public Date getLowerBoundAsInstant() {
if (myLowerBound == null) {
return null;
@ -204,19 +215,19 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
Date retVal = myLowerBound.getValue();
if (myLowerBound.getPrefix() != null) {
switch (myLowerBound.getPrefix()) {
case GREATERTHAN:
case STARTS_AFTER:
retVal = myLowerBound.getPrecision().add(retVal, 1);
break;
case EQUAL:
case GREATERTHAN_OR_EQUALS:
break;
case LESSTHAN:
case APPROXIMATE:
case LESSTHAN_OR_EQUALS:
case ENDS_BEFORE:
case NOT_EQUAL:
throw new IllegalStateException("Unvalid lower bound comparator: " + myLowerBound.getPrefix());
case GREATERTHAN:
case STARTS_AFTER:
retVal = myLowerBound.getPrecision().add(retVal, 1);
break;
case EQUAL:
case GREATERTHAN_OR_EQUALS:
break;
case LESSTHAN:
case APPROXIMATE:
case LESSTHAN_OR_EQUALS:
case ENDS_BEFORE:
case NOT_EQUAL:
throw new IllegalStateException("Unvalid lower bound comparator: " + myLowerBound.getPrefix());
}
}
return retVal;
@ -226,6 +237,11 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
return myUpperBound;
}
public DateRangeParam setUpperBound(DateParam theUpperBound) {
validateAndSet(myLowerBound, theUpperBound);
return this;
}
public Date getUpperBoundAsInstant() {
if (myUpperBound == null) {
return null;
@ -233,21 +249,21 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
Date retVal = myUpperBound.getValue();
if (myUpperBound.getPrefix() != null) {
switch (myUpperBound.getPrefix()) {
case LESSTHAN:
case ENDS_BEFORE:
retVal = new Date(retVal.getTime() - 1L);
break;
case EQUAL:
case LESSTHAN_OR_EQUALS:
retVal = myUpperBound.getPrecision().add(retVal, 1);
retVal = new Date(retVal.getTime() - 1L);
break;
case GREATERTHAN_OR_EQUALS:
case GREATERTHAN:
case APPROXIMATE:
case NOT_EQUAL:
case STARTS_AFTER:
throw new IllegalStateException("Unvalid upper bound comparator: " + myUpperBound.getPrefix());
case LESSTHAN:
case ENDS_BEFORE:
retVal = new Date(retVal.getTime() - 1L);
break;
case EQUAL:
case LESSTHAN_OR_EQUALS:
retVal = myUpperBound.getPrecision().add(retVal, 1);
retVal = new Date(retVal.getTime() - 1L);
break;
case GREATERTHAN_OR_EQUALS:
case GREATERTHAN:
case APPROXIMATE:
case NOT_EQUAL:
case STARTS_AFTER:
throw new IllegalStateException("Unvalid upper bound comparator: " + myUpperBound.getPrefix());
}
}
return retVal;
@ -273,46 +289,55 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
return bound != null && !bound.isEmpty();
}
@Override
public int hashCode() {
return Objects.hash(myLowerBound, myUpperBound);
}
public boolean isEmpty() {
return (getLowerBoundAsInstant() == null) && (getUpperBoundAsInstant() == null);
}
public DateRangeParam setLowerBound(DateParam theLowerBound) {
validateAndSet(theLowerBound, myUpperBound);
/**
* Sets the lower bound using a string that is compliant with
* FHIR dateTime format (ISO-8601).
* <p>
* This lower bound is assumed to have a <code>ge</code>
* (greater than or equals) modifier.
* </p>
*/
public DateRangeParam setLowerBound(String theLowerBound) {
setLowerBound(new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound));
return this;
}
/**
* Sets the range from a pair of dates, inclusive on both ends
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public void setRangeFromDatesInclusive(Date theLowerBound, Date theUpperBound) {
DateParam lowerBound = theLowerBound != null
? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null;
? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound) : null;
DateParam upperBound = theUpperBound != null
? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null;
? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound) : null;
validateAndSet(lowerBound, upperBound);
}
/**
* Sets the range from a pair of dates, inclusive on both ends
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public void setRangeFromDatesInclusive(DateParam theLowerBound, DateParam theUpperBound) {
validateAndSet(theLowerBound, theUpperBound);
@ -323,14 +348,12 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
* theLowerBound is after theUpperBound, thie method will automatically reverse
* the order of the arguments in order to create an inclusive range.
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public void setRangeFromDatesInclusive(IPrimitiveType<Date> theLowerBound, IPrimitiveType<Date> theUpperBound) {
IPrimitiveType<Date> lowerBound = theLowerBound;
@ -350,22 +373,20 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
/**
* Sets the range from a pair of dates, inclusive on both ends
*
* @param theLowerBound
* A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound
* A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theLowerBound A qualified date param representing the lower date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
* @param theUpperBound A qualified date param representing the upper date bound (optionally may include time), e.g.
* "2011-02-22" or "2011-02-22T13:12:00Z". Will be treated inclusively. Either theLowerBound or
* theUpperBound may both be populated, or one may be null, but it is not valid for both to be null.
*/
public void setRangeFromDatesInclusive(String theLowerBound, String theUpperBound) {
DateParam lowerBound = theLowerBound != null
? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound)
: null;
? new DateParam(GREATERTHAN_OR_EQUALS, theLowerBound)
: null;
DateParam upperBound = theUpperBound != null
? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound)
: null;
? new DateParam(LESSTHAN_OR_EQUALS, theUpperBound)
: null;
if (isNotBlank(theLowerBound) && isNotBlank(theUpperBound) && theLowerBound.equals(theUpperBound)) {
lowerBound.setPrefix(EQUAL);
upperBound.setPrefix(EQUAL);
@ -373,14 +394,22 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
validateAndSet(lowerBound, upperBound);
}
public DateRangeParam setUpperBound(DateParam theUpperBound) {
validateAndSet(myLowerBound, theUpperBound);
/**
* Sets the upper bound using a string that is compliant with
* FHIR dateTime format (ISO-8601).
* <p>
* This upper bound is assumed to have a <code>le</code>
* (less than or equals) modifier.
* </p>
*/
public DateRangeParam setUpperBound(String theUpperBound) {
setUpperBound(new DateParam(LESSTHAN_OR_EQUALS, theUpperBound));
return this;
}
@Override
public void setValuesAsQueryTokens(FhirContext theContext, String theParamName, List<QualifiedParamList> theParameters)
throws InvalidRequestException {
throws InvalidRequestException {
boolean haveHadUnqualifiedParameter = false;
for (QualifiedParamList paramList : theParameters) {
@ -413,24 +442,6 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof DateRangeParam)) {
return false;
}
DateRangeParam other = (DateRangeParam) obj;
return Objects.equals(myLowerBound, other.myLowerBound) &&
Objects.equals(myUpperBound, other.myUpperBound);
}
@Override
public int hashCode() {
return Objects.hash(myLowerBound, myUpperBound);
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
@ -463,8 +474,8 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
if (hasBound(lowerBound) && hasBound(upperBound)) {
if (lowerBound.getValue().getTime() > upperBound.getValue().getTime()) {
throw new DataFormatException(format(
"Lower bound of %s is after upper bound of %s",
lowerBound.getValueAsString(), upperBound.getValueAsString()));
"Lower bound of %s is after upper bound of %s",
lowerBound.getValueAsString(), upperBound.getValueAsString()));
}
}
@ -473,13 +484,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
lowerBound.setPrefix(GREATERTHAN_OR_EQUALS);
}
switch (lowerBound.getPrefix()) {
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
default:
break;
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
throw new DataFormatException("Lower bound comparator must be > or >=, can not be " + lowerBound.getPrefix().getValue());
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
default:
break;
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
throw new DataFormatException("Lower bound comparator must be > or >=, can not be " + lowerBound.getPrefix().getValue());
}
}
@ -488,13 +499,13 @@ public class DateRangeParam implements IQueryParameterAnd<DateParam> {
upperBound.setPrefix(LESSTHAN_OR_EQUALS);
}
switch (upperBound.getPrefix()) {
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
default:
break;
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
throw new DataFormatException("Upper bound comparator must be < or <=, can not be " + upperBound.getPrefix().getValue());
case LESSTHAN:
case LESSTHAN_OR_EQUALS:
default:
break;
case GREATERTHAN:
case GREATERTHAN_OR_EQUALS:
throw new DataFormatException("Upper bound comparator must be < or <=, can not be " + upperBound.getPrefix().getValue());
}
}

View File

@ -86,7 +86,7 @@ public class ReflectionUtil {
public static Class<?> getGenericCollectionTypeOfMethodParameter(Method theMethod, int theParamIndex) {
Class<?> type;
Type genericParameterType = theMethod.getGenericParameterTypes()[theParamIndex];
if (Class.class.equals(genericParameterType)) {
if (Class.class.equals(genericParameterType) || Class.class.equals(genericParameterType.getClass())) {
return null;
}
ParameterizedType collectionType = (ParameterizedType) genericParameterType;

View File

@ -57,6 +57,7 @@ ca.uhn.fhir.validation.ValidationResult.noIssuesDetected=No issues detected duri
# JPA Messages
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceVersionConstraintFailure=The operation has failed with a version constraint failure. This generally means that two clients/threads were trying to update the same resource at the same time, and this request was chosen as the failing request.
ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceIndexedCompositeStringUniqueConstraintFailure=The operation has failed with a unique index constraint failure. This probably means that the operation was trying to create/update a resource that would have resulted in a duplicate value for a unique index.
@ -90,9 +91,13 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulUpdate=Successfully update
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.successfulDeletes=Successfully deleted {0} resource(s) in {1}ms
ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao.invalidSearchParameter=Unknown search parameter "{0}". Value search parameters for this search are: {1}
ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor.failedToExtractPaths=Failed to extract values from resource using FHIRPath "{0}": {1}
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidQuantityPrefix=Unable to handle quantity prefix "{0}" for value: {1}
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidNumberPrefix=Unable to handle number prefix "{0}" for value: {1}
ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoSearchParameterR4.invalidSearchParamExpression=The expression "{0}" can not be evaluated and may be invalid: {1}
ca.uhn.fhir.jpa.provider.BaseJpaProvider.cantCombintAtAndSince=Unable to combine _at and _since parameters for history operation
ca.uhn.fhir.jpa.term.BaseHapiTerminologySvcImpl.cannotCreateDuplicateConceptMapUrl=Can not create multiple ConceptMap resources with ConceptMap.url "{0}", already have one with resource ID: {1}

View File

@ -743,6 +743,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private Class<? extends IBaseBundle> myReturnType;
private IPrimitiveType mySince;
private Class<? extends IBaseResource> myType;
private DateRangeParam myAt;
@SuppressWarnings("unchecked")
@Override
@ -752,6 +753,12 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this;
}
@Override
public IHistoryTyped at(DateRangeParam theDateRangeParam) {
myAt = theDateRangeParam;
return this;
}
@Override
public IHistoryTyped count(Integer theCount) {
myCount = theCount;
@ -774,7 +781,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
id = null;
}
HttpGetClientInvocation invocation = HistoryMethodBinding.createHistoryInvocation(myContext, resourceName, id, mySince, myCount);
HttpGetClientInvocation invocation = HistoryMethodBinding.createHistoryInvocation(myContext, resourceName, id, mySince, myCount, myAt);
IClientResponseHandler handler;
handler = new ResourceResponseHandler(myReturnType, getPreferResponseTypes(myType));

View File

@ -26,6 +26,8 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Date;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import org.hl7.fhir.instance.model.api.*;
import ca.uhn.fhir.context.FhirContext;
@ -96,7 +98,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
String historyId = id != null ? id.getIdPart() : null;
HttpGetClientInvocation retVal = createHistoryInvocation(getContext(), resourceName, historyId, null, null);
HttpGetClientInvocation retVal = createHistoryInvocation(getContext(), resourceName, historyId, null, null, null);
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
@ -108,7 +110,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
return retVal;
}
public static HttpGetClientInvocation createHistoryInvocation(FhirContext theContext, String theResourceName, String theId, IPrimitiveType<Date> theSince, Integer theLimit) {
public static HttpGetClientInvocation createHistoryInvocation(FhirContext theContext, String theResourceName, String theId, IPrimitiveType<Date> theSince, Integer theLimit, DateRangeParam theAt) {
StringBuilder b = new StringBuilder();
if (theResourceName != null) {
b.append(theResourceName);
@ -129,8 +131,18 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
if (theLimit != null) {
b.append(haveParam ? '&' : '?');
haveParam = true;
b.append(Constants.PARAM_COUNT).append('=').append(theLimit);
}
if (theAt != null) {
for (DateParam next : theAt.getValuesAsQueryTokens()) {
b.append(haveParam ? '&' : '?');
haveParam = true;
b.append(Constants.PARAM_AT);
b.append("=");
b.append(next.getValueAsQueryToken(theContext));
}
}
HttpGetClientInvocation retVal = new HttpGetClientInvocation(theContext, b.toString());
return retVal;

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.FhirTerser;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.ObjectUtils;
@ -47,6 +48,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
private DaoConfig myDaoConfig;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
public BaseSearchParamExtractor() {
super();
}
@ -73,31 +75,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
return refs;
}
protected List<Object> extractValues(String thePaths, IBaseResource theResource) {
List<Object> values = new ArrayList<Object>();
String[] nextPathsSplit = SPLIT.split(thePaths);
FhirTerser t = myContext.newTerser();
for (String nextPath : nextPathsSplit) {
String nextPathTrimmed = nextPath.trim();
try {
List<Object> allValues = t.getValues(theResource, nextPathTrimmed);
for (Object next : allValues) {
if (next instanceof IBaseExtension) {
IBaseDatatype value = ((IBaseExtension) next).getValue();
if (value != null) {
values.add(value);
}
} else {
values.add(next);
}
}
} catch (Exception e) {
RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
ourLog.warn("Failed to index values from path[{}] in resource type[{}]: {}", new Object[] {nextPathTrimmed, def.getName(), e.toString(), e});
}
}
return values;
}
protected abstract List<Object> extractValues(String thePaths, IBaseResource theResource);
protected FhirContext getContext() {
return myContext;

View File

@ -144,6 +144,7 @@ public class DaoConfig {
private boolean myExpungeEnabled;
private int myReindexThreadCount;
private Set<String> myBundleTypesAllowedForStorage;
private boolean myValidateSearchParameterExpressionsOnSave = true;
/**
* Constructor
@ -786,7 +787,6 @@ public class DaoConfig {
this.myAllowContainsSearches = theAllowContainsSearches;
}
/**
* If set to <code>true</code> (default is <code>false</code>) the server will allow
* resources to have references to external servers. For example if this server is
@ -1188,6 +1188,34 @@ public class DaoConfig {
myUniqueIndexesEnabled = theUniqueIndexesEnabled;
}
/**
* If <code>true</code> (default is <code>true</code>), before allowing a
* SearchParameter resource to be stored (create, update, etc.) the
* expression will be performed against an empty resource to ensure that
* the FHIRPath executor is able to process it.
* <p>
* This should proabably always be set to true, but is configurable
* in order to support some unit tests.
* </p>
*/
public boolean isValidateSearchParameterExpressionsOnSave() {
return myValidateSearchParameterExpressionsOnSave;
}
/**
* If <code>true</code> (default is <code>true</code>), before allowing a
* SearchParameter resource to be stored (create, update, etc.) the
* expression will be performed against an empty resource to ensure that
* the FHIRPath executor is able to process it.
* <p>
* This should proabably always be set to true, but is configurable
* in order to support some unit tests.
* </p>
*/
public void setValidateSearchParameterExpressionsOnSave(boolean theValidateSearchParameterExpressionsOnSave) {
myValidateSearchParameterExpressionsOnSave = theValidateSearchParameterExpressionsOnSave;
}
/**
* Do not call this method, it exists only for legacy reasons. It
* will be removed in a future version. Configure the page size on your

View File

@ -29,14 +29,8 @@ import ca.uhn.fhir.model.dstu2.resource.SearchParameter;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum;
import ca.uhn.fhir.model.primitive.BoundCodeDt;
import ca.uhn.fhir.model.primitive.CodeDt;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -83,7 +77,7 @@ public class FhirResourceDaoSearchParameterDstu2 extends FhirResourceDaoDstu2<Se
FhirContext context = getContext();
SearchParamTypeEnum type = theResource.getTypeElement().getValueAsEnum();
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context);
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context, getConfig());
}

View File

@ -19,33 +19,40 @@ package ca.uhn.fhir.jpa.dao;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.math.BigDecimal;
import java.util.*;
import javax.measure.quantity.Quantity;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.api.IDatatype;
import ca.uhn.fhir.model.api.IPrimitiveDatatype;
import ca.uhn.fhir.model.api.IValueSetEnumBinder;
import ca.uhn.fhir.model.base.composite.BaseHumanNameDt;
import ca.uhn.fhir.model.dstu2.composite.*;
import ca.uhn.fhir.model.dstu2.composite.BoundCodeableConceptDt;
import ca.uhn.fhir.model.dstu2.resource.*;
import ca.uhn.fhir.model.dstu2.resource.Conformance.RestSecurity;
import ca.uhn.fhir.model.dstu2.resource.Location;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.resource.Patient.Communication;
import ca.uhn.fhir.model.dstu2.resource.Questionnaire;
import ca.uhn.fhir.model.dstu2.resource.ValueSet;
import ca.uhn.fhir.model.dstu2.valueset.RestfulSecurityServiceEnum;
import ca.uhn.fhir.model.primitive.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.FhirTerser;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.measure.quantity.Quantity;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import java.math.BigDecimal;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implements ISearchParamExtractor {
@ -87,7 +94,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
*/
@Override
public Set<ResourceIndexedSearchParamDate> extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) {
HashSet<ResourceIndexedSearchParamDate> retVal = new HashSet<ResourceIndexedSearchParamDate>();
HashSet<ResourceIndexedSearchParamDate> retVal = new HashSet<>();
Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
for (RuntimeSearchParam nextSpDef : searchParams) {
@ -626,6 +633,35 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
}
}
@Override
protected List<Object> extractValues(String thePaths, IBaseResource theResource) {
List<Object> values = new ArrayList<>();
String[] nextPathsSplit = SPLIT.split(thePaths);
FhirTerser t = getContext().newTerser();
for (String nextPath : nextPathsSplit) {
String nextPathTrimmed = nextPath.trim();
List<Object> allValues;
try {
allValues = t.getValues(theResource, nextPathTrimmed);
} catch (Exception e) {
String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString());
throw new InternalErrorException(msg, e);
}
for (Object next : allValues) {
if (next instanceof IBaseExtension) {
IBaseDatatype value = ((IBaseExtension) next).getValue();
if (value != null) {
values.add(value);
}
} else {
values.add(next);
}
}
}
return values;
}
private static <T extends Enum<?>> String extractSystem(BoundCodeDt<T> theBoundCode) {
if (theBoundCode.getValueAsEnum() != null) {
IValueSetEnumBinder<T> binder = theBoundCode.getBinder();

View File

@ -74,7 +74,7 @@ public class FhirResourceDaoSearchParameterDstu3 extends FhirResourceDaoDstu3<Se
FhirContext context = getContext();
Enumerations.SearchParamType type = theResource.getType();
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context);
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context, getConfig());
}
}

View File

@ -19,17 +19,15 @@ package ca.uhn.fhir.jpa.dao.dstu3;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trim;
import java.math.BigDecimal;
import java.util.*;
import javax.annotation.PostConstruct;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
@ -41,16 +39,18 @@ import org.hl7.fhir.dstu3.model.Location.LocationPositionComponent;
import org.hl7.fhir.dstu3.model.Patient.PatientCommunicationComponent;
import org.hl7.fhir.dstu3.utils.FHIRPathEngine;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.springframework.beans.factory.annotation.Autowired;
import com.google.common.annotations.VisibleForTesting;
import javax.annotation.PostConstruct;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import java.math.BigDecimal;
import java.util.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import static org.apache.commons.lang3.StringUtils.*;
public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implements ISearchParamExtractor {
@ -61,11 +61,6 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
private HapiWorkerContext myWorkerContext;
@PostConstruct
public void start() {
myWorkerContext = new HapiWorkerContext(getContext(), myValidationSupport);
}
/**
* Constructor
*/
@ -78,6 +73,17 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
myValidationSupport = theValidationSupport;
}
private void addQuantity(ResourceTable theEntity, HashSet<ResourceIndexedSearchParamQuantity> retVal, String resourceName, Quantity nextValue) {
if (!nextValue.getValueElement().isEmpty()) {
BigDecimal nextValueValue = nextValue.getValueElement().getValue();
String nextValueString = nextValue.getSystemElement().getValueAsString();
String nextValueCode = nextValue.getCode();
ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValueValue, nextValueString, nextValueCode);
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
}
}
private void addSearchTerm(ResourceTable theEntity, Set<ResourceIndexedSearchParamString> retVal, String resourceName, String searchTerm) {
if (isBlank(searchTerm)) {
return;
@ -100,6 +106,23 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
retVal.add(nextEntity);
}
@Override
public List<PathAndRef> extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) {
ArrayList<PathAndRef> retVal = new ArrayList<PathAndRef>();
String[] nextPathsSplit = SPLIT.split(theNextSpDef.getPath());
for (String path : nextPathsSplit) {
path = path.trim();
if (isNotBlank(path)) {
for (Object next : extractValues(path, theResource)) {
retVal.add(new PathAndRef(path, next));
}
}
}
return retVal;
}
@Override
public Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
// TODO: implement
@ -200,7 +223,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
*/
@Override
public HashSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) {
HashSet<ResourceIndexedSearchParamNumber> retVal = new HashSet<ResourceIndexedSearchParamNumber>();
HashSet<ResourceIndexedSearchParamNumber> retVal = new HashSet<>();
Collection<RuntimeSearchParam> searchParams = getSearchParams(theResource);
for (RuntimeSearchParam nextSpDef : searchParams) {
@ -349,17 +372,6 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
return retVal;
}
private void addQuantity(ResourceTable theEntity, HashSet<ResourceIndexedSearchParamQuantity> retVal, String resourceName, Quantity nextValue) {
if (!nextValue.getValueElement().isEmpty()) {
BigDecimal nextValueValue = nextValue.getValueElement().getValue();
String nextValueString = nextValue.getSystemElement().getValueAsString();
String nextValueCode = nextValue.getCode();
ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValueValue, nextValueString, nextValueCode);
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
}
}
/*
* (non-Javadoc)
*
@ -674,14 +686,14 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
}
private void extractTokensFromCodeableConcept(List<String> theSystems, List<String> theCodes, CodeableConcept theCodeableConcept, ResourceTable theEntity,
Set<BaseResourceIndexedSearchParam> theListToPopulate, RuntimeSearchParam theParameterDef) {
Set<BaseResourceIndexedSearchParam> theListToPopulate, RuntimeSearchParam theParameterDef) {
for (Coding nextCoding : theCodeableConcept.getCoding()) {
extractTokensFromCoding(theSystems, theCodes, theEntity, theListToPopulate, theParameterDef, nextCoding);
}
}
private void extractTokensFromCoding(List<String> theSystems, List<String> theCodes, ResourceTable theEntity, Set<BaseResourceIndexedSearchParam> theListToPopulate,
RuntimeSearchParam theParameterDef, Coding nextCoding) {
RuntimeSearchParam theParameterDef, Coding nextCoding) {
if (nextCoding != null && !nextCoding.isEmpty()) {
String nextSystem = nextCoding.getSystemElement().getValueAsString();
@ -706,16 +718,18 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
FHIRPathEngine fp = new FHIRPathEngine(myWorkerContext);
List<Object> values = new ArrayList<>();
try {
String[] nextPathsSplit = SPLIT.split(thePaths);
for (String nextPath : nextPathsSplit) {
List<Base> allValues = fp.evaluate((Base) theResource, trim(nextPath));
if (allValues.isEmpty() == false) {
values.addAll(allValues);
}
String[] nextPathsSplit = SPLIT.split(thePaths);
for (String nextPath : nextPathsSplit) {
List<Base> allValues;
try {
allValues = fp.evaluate((Base) theResource, trim(nextPath));
} catch (FHIRException e) {
String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString());
throw new InternalErrorException(msg, e);
}
if (allValues.isEmpty() == false) {
values.addAll(allValues);
}
} catch (FHIRException e) {
throw new InternalErrorException(e);
}
for (int i = 0; i < values.size(); i++) {
@ -730,28 +744,16 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
return values;
}
@Override
public List<PathAndRef> extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) {
ArrayList<PathAndRef> retVal = new ArrayList<PathAndRef>();
String[] nextPathsSplit = SPLIT.split(theNextSpDef.getPath());
for (String path : nextPathsSplit) {
path = path.trim();
if (isNotBlank(path)) {
for (Object next : extractValues(path, theResource)) {
retVal.add(new PathAndRef(path, next));
}
}
}
return retVal;
}
@VisibleForTesting
void setValidationSupportForTesting(org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport theValidationSupport) {
myValidationSupport = theValidationSupport;
}
@PostConstruct
public void start() {
myWorkerContext = new HapiWorkerContext(getContext(), myValidationSupport);
}
private static <T extends Enum<?>> String extractSystem(Enumeration<T> theBoundCode) {
if (theBoundCode.getValue() != null) {
return theBoundCode.getEnumFactory().toSystem(theBoundCode.getValue());

View File

@ -1,13 +1,17 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.dao.BaseSearchParamExtractor;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.ElementUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.*;
import org.springframework.beans.factory.annotation.Autowired;
@ -76,10 +80,10 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4<SearchPa
FhirContext context = getContext();
Enum<?> type = theResource.getType();
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context);
FhirResourceDaoSearchParameterR4.validateSearchParam(type, status, base, expression, context, getConfig());
}
public static void validateSearchParam(Enum<?> theType, Enum<?> theStatus, List<? extends IPrimitiveType> theBase, String theExpression, FhirContext theContext) {
public static void validateSearchParam(Enum<?> theType, Enum<?> theStatus, List<? extends IPrimitiveType> theBase, String theExpression, FhirContext theContext, DaoConfig theDaoConfig) {
if (theStatus == null) {
throw new UnprocessableEntityException("SearchParameter.status is missing or invalid");
}
@ -116,6 +120,17 @@ public class FhirResourceDaoSearchParameterR4 extends FhirResourceDaoR4<SearchPa
throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + nextPath + "\": " + e.getMessage());
}
if (theContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
if (theDaoConfig.isValidateSearchParameterExpressionsOnSave()) {
IBaseResource temporaryInstance = theContext.getResourceDefinition(resourceName).newInstance();
try {
theContext.newFluentPath().evaluate(temporaryInstance, nextPath, IBase.class);
} catch (Exception e) {
String msg = theContext.getLocalizer().getMessage(FhirResourceDaoSearchParameterR4.class, "invalidSearchParamExpression", nextPath, e.getMessage());
throw new UnprocessableEntityException(msg);
}
}
}
}
} // if have expression

View File

@ -19,17 +19,21 @@ package ca.uhn.fhir.jpa.dao.r4;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.math.BigDecimal;
import java.util.*;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.hapi.ctx.IValidationSupport;
import org.hl7.fhir.r4.model.*;
@ -38,17 +42,15 @@ import org.hl7.fhir.r4.model.Enumeration;
import org.hl7.fhir.r4.model.Location.LocationPositionComponent;
import org.hl7.fhir.r4.model.Patient.PatientCommunicationComponent;
import org.hl7.fhir.r4.utils.FHIRPathEngine;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import com.google.common.annotations.VisibleForTesting;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import java.math.BigDecimal;
import java.util.*;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements ISearchParamExtractor {
@ -69,6 +71,17 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
myValidationSupport = theValidationSupport;
}
private void addQuantity(ResourceTable theEntity, HashSet<ResourceIndexedSearchParamQuantity> retVal, String resourceName, Quantity nextValue) {
if (!nextValue.getValueElement().isEmpty()) {
BigDecimal nextValueValue = nextValue.getValueElement().getValue();
String nextValueString = nextValue.getSystemElement().getValueAsString();
String nextValueCode = nextValue.getCode();
ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValueValue, nextValueString, nextValueCode);
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
}
}
private void addSearchTerm(ResourceTable theEntity, Set<ResourceIndexedSearchParamString> retVal, String resourceName, String searchTerm) {
if (isBlank(searchTerm)) {
return;
@ -91,6 +104,23 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
retVal.add(nextEntity);
}
@Override
public List<PathAndRef> extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) {
ArrayList<PathAndRef> retVal = new ArrayList<>();
String[] nextPathsSplit = SPLIT.split(theNextSpDef.getPath());
for (String path : nextPathsSplit) {
path = path.trim();
if (isNotBlank(path)) {
for (Object next : extractValues(path, theResource)) {
retVal.add(new PathAndRef(path, next));
}
}
}
return retVal;
}
@Override
public Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
// TODO: implement
@ -336,17 +366,6 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
return retVal;
}
private void addQuantity(ResourceTable theEntity, HashSet<ResourceIndexedSearchParamQuantity> retVal, String resourceName, Quantity nextValue) {
if (!nextValue.getValueElement().isEmpty()) {
BigDecimal nextValueValue = nextValue.getValueElement().getValue();
String nextValueString = nextValue.getSystemElement().getValueAsString();
String nextValueCode = nextValue.getCode();
ResourceIndexedSearchParamQuantity nextEntity = new ResourceIndexedSearchParamQuantity(resourceName, nextValueValue, nextValueString, nextValueCode);
nextEntity.setResource(theEntity);
retVal.add(nextEntity);
}
}
/*
* (non-Javadoc)
*
@ -658,14 +677,14 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
}
private void extractTokensFromCodeableConcept(List<String> theSystems, List<String> theCodes, CodeableConcept theCodeableConcept, ResourceTable theEntity,
Set<BaseResourceIndexedSearchParam> theListToPopulate, RuntimeSearchParam theParameterDef) {
Set<BaseResourceIndexedSearchParam> theListToPopulate, RuntimeSearchParam theParameterDef) {
for (Coding nextCoding : theCodeableConcept.getCoding()) {
extractTokensFromCoding(theSystems, theCodes, theEntity, theListToPopulate, theParameterDef, nextCoding);
}
}
private void extractTokensFromCoding(List<String> theSystems, List<String> theCodes, ResourceTable theEntity, Set<BaseResourceIndexedSearchParam> theListToPopulate,
RuntimeSearchParam theParameterDef, Coding nextCoding) {
RuntimeSearchParam theParameterDef, Coding nextCoding) {
if (nextCoding != null && !nextCoding.isEmpty()) {
String nextSystem = nextCoding.getSystemElement().getValueAsString();
@ -691,16 +710,18 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
FHIRPathEngine fp = new FHIRPathEngine(worker);
List<Object> values = new ArrayList<>();
try {
String[] nextPathsSplit = SPLIT.split(thePaths);
for (String nextPath : nextPathsSplit) {
List<Base> allValues = fp.evaluate((Base) theResource, nextPath);
if (allValues.isEmpty() == false) {
values.addAll(allValues);
}
String[] nextPathsSplit = SPLIT.split(thePaths);
for (String nextPath : nextPathsSplit) {
List<Base> allValues;
try {
allValues = fp.evaluate((Base) theResource, nextPath);
} catch (FHIRException e) {
String msg = getContext().getLocalizer().getMessage(BaseSearchParamExtractor.class, "failedToExtractPaths", nextPath, e.toString());
throw new InternalErrorException(msg, e);
}
if (allValues.isEmpty() == false) {
values.addAll(allValues);
}
} catch (FHIRException e) {
throw new InternalErrorException(e);
}
for (int i = 0; i < values.size(); i++) {
@ -715,23 +736,6 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
return values;
}
@Override
public List<PathAndRef> extractResourceLinks(IBaseResource theResource, RuntimeSearchParam theNextSpDef) {
ArrayList<PathAndRef> retVal = new ArrayList<>();
String[] nextPathsSplit = SPLIT.split(theNextSpDef.getPath());
for (String path : nextPathsSplit) {
path = path.trim();
if (isNotBlank(path)) {
for (Object next : extractValues(path, theResource)) {
retVal.add(new PathAndRef(path, next));
}
}
}
return retVal;
}
@VisibleForTesting
void setValidationSupportForTesting(org.hl7.fhir.r4.hapi.ctx.IValidationSupport theValidationSupport) {
myValidationSupport = theValidationSupport;

View File

@ -62,6 +62,9 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest {
@Qualifier("mySearchParameterDaoDstu2")
protected IFhirResourceDao<SearchParameter> mySearchParameterDao;
@Autowired
@Qualifier("myCommunicationDaoDstu2")
protected IFhirResourceDao<Communication> myCommunicationDao;
@Autowired
@Qualifier("myBundleDaoDstu2")
protected IFhirResourceDao<Bundle> myBundleDao;
@Autowired

View File

@ -13,14 +13,12 @@ import ca.uhn.fhir.model.dstu2.valueset.*;
import ca.uhn.fhir.model.primitive.*;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.*;
import org.mockito.internal.util.collections.ListUtil;
import java.util.List;
@ -37,6 +35,12 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden());
}
@After
public void after() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(new DaoConfig().isValidateSearchParameterExpressionsOnSave());
}
@Test
public void testCreateInvalidNoBase() {
SearchParameter fooSp = new SearchParameter();
@ -53,6 +57,31 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
}
}
@Test
public void testIndexFailsIfInvalidSearchParameterExists() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(false);
SearchParameter threadIdSp = new SearchParameter();
threadIdSp.setBase(ResourceTypeEnum.COMMUNICATION);
threadIdSp.setCode("has-attachments");
threadIdSp.setType(SearchParamTypeEnum.REFERENCE);
threadIdSp.setXpath("Communication.payload[1].contentAttachment is not null");
threadIdSp.setXpathUsage(XPathUsageTypeEnum.NORMAL);
threadIdSp.setStatus(ConformanceResourceStatusEnum.ACTIVE);
mySearchParameterDao.create(threadIdSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Communication com = new Communication();
com.setStatus(CommunicationStatusEnum.IN_PROGRESS);
try {
myCommunicationDao.create(com, mySrd);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), startsWith("Failed to extract values from resource using FHIRPath \"Communication.payload[1].contentAttachment is not null\": ca.uhn"));
}
}
@Test
public void testCreateInvalidParamInvalidResourceName() {
SearchParameter fooSp = new SearchParameter();

View File

@ -168,6 +168,9 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
@Qualifier("myPatientDaoDstu3")
protected IFhirResourceDaoPatient<Patient> myPatientDao;
@Autowired
@Qualifier("myCommunicationDaoDstu3")
protected IFhirResourceDao<Communication> myCommunicationDao;
@Autowired
@Qualifier("myPractitionerDaoDstu3")
protected IFhirResourceDao<Practitioner> myPractitionerDao;
@Autowired

View File

@ -1,9 +1,11 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
@ -12,6 +14,7 @@ import org.hl7.fhir.dstu3.model.Appointment.AppointmentStatus;
import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
@ -29,6 +32,11 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
}
@After
public void after() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(new DaoConfig().isValidateSearchParameterExpressionsOnSave());
}
@Test
public void testCreateInvalidNoBase() {
SearchParameter fooSp = new SearchParameter();
@ -211,6 +219,47 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu
}
@Test
public void testIndexFailsIfInvalidSearchParameterExists() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(false);
SearchParameter threadIdSp = new SearchParameter();
threadIdSp.addBase("Communication");
threadIdSp.setCode("has-attachments");
threadIdSp.setType(Enumerations.SearchParamType.REFERENCE);
threadIdSp.setExpression("Communication.payload[1].contentAttachment is not null");
threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
threadIdSp.setStatus(Enumerations.PublicationStatus.ACTIVE);
mySearchParameterDao.create(threadIdSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Communication com = new Communication();
com.setStatus(Communication.CommunicationStatus.INPROGRESS);
try {
myCommunicationDao.create(com, mySrd);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), startsWith("Failed to extract values from resource using FHIRPath \"Communication.payload[1].contentAttachment is not null\": org.hl7.fhir"));
}
}
@Test
public void testRejectSearchParamWithInvalidExpression() {
SearchParameter threadIdSp = new SearchParameter();
threadIdSp.addBase("Communication");
threadIdSp.setCode("has-attachments");
threadIdSp.setType(Enumerations.SearchParamType.REFERENCE);
threadIdSp.setExpression("Communication.payload[1].contentAttachment is not null");
threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
threadIdSp.setStatus(Enumerations.PublicationStatus.ACTIVE);
try {
mySearchParameterDao.create(threadIdSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), startsWith("The expression \"Communication.payload[1].contentAttachment is not null\" can not be evaluated and may be invalid: "));
}
}
/**
* See #863
*/

View File

@ -92,6 +92,9 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
@Qualifier("myBundleDaoR4")
protected IFhirResourceDao<Bundle> myBundleDao;
@Autowired
@Qualifier("myCommunicationDaoR4")
protected IFhirResourceDao<Communication> myCommunicationDao;
@Autowired
@Qualifier("myCarePlanDaoR4")
protected IFhirResourceDao<CarePlan> myCarePlanDao;
@Autowired

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
@ -13,6 +14,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Appointment.AppointmentStatus;
import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
@ -30,6 +32,11 @@ import static org.junit.Assert.*;
public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4SearchCustomSearchParamTest.class);
@After
public void after() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(new DaoConfig().isValidateSearchParameterExpressionsOnSave());
}
@Before
public void beforeDisableResultReuse() {
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
@ -71,7 +78,6 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
}
}
@Test
public void testCreateInvalidParamNoPath() {
SearchParameter fooSp = new SearchParameter();
@ -237,6 +243,30 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
}
@Test
public void testIndexFailsIfInvalidSearchParameterExists() {
myDaoConfig.setValidateSearchParameterExpressionsOnSave(false);
SearchParameter threadIdSp = new SearchParameter();
threadIdSp.addBase("Communication");
threadIdSp.setCode("has-attachments");
threadIdSp.setType(Enumerations.SearchParamType.REFERENCE);
threadIdSp.setExpression("Communication.payload[1].contentAttachment is not null");
threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
threadIdSp.setStatus(Enumerations.PublicationStatus.ACTIVE);
mySearchParameterDao.create(threadIdSp, mySrd);
mySearchParamRegsitry.forceRefresh();
Communication com = new Communication();
com.setStatus(Communication.CommunicationStatus.INPROGRESS);
try {
myCommunicationDao.create(com, mySrd);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), startsWith("Failed to extract values from resource using FHIRPath \"Communication.payload[1].contentAttachment is not null\": org.hl7.fhir"));
}
}
@Test
public void testOverrideAndDisableBuiltInSearchParametersWithOverridingDisabled() {
myDaoConfig.setDefaultSearchParamsCanBeOverridden(false);
@ -387,6 +417,23 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
assertThat(results, contains(mrId));
}
@Test
public void testRejectSearchParamWithInvalidExpression() {
SearchParameter threadIdSp = new SearchParameter();
threadIdSp.addBase("Communication");
threadIdSp.setCode("has-attachments");
threadIdSp.setType(Enumerations.SearchParamType.REFERENCE);
threadIdSp.setExpression("Communication.payload[1].contentAttachment is not null");
threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
threadIdSp.setStatus(Enumerations.PublicationStatus.ACTIVE);
try {
mySearchParameterDao.create(threadIdSp, mySrd);
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.getMessage(), startsWith("The expression \"Communication.payload[1].contentAttachment is not null\" can not be evaluated and may be invalid: "));
}
}
@Test
public void testSearchForExtensionReferenceWithNonMatchingTarget() {
SearchParameter siblingSp = new SearchParameter();

View File

@ -0,0 +1,46 @@
{
"resourceType": "Communication",
"meta": {
"lastUpdated": "2018-07-20T19:34:56.236+05:30",
"tag": [
{
"system": "systemDefined",
"code": "read"
}
]
},
"text": {
"status": "generated"
},
"extension": [
{
"url": "http://telus.com/fhir/StructureDefinition/ext-communication-msgOwner",
"valueString": "17427"
},
{
"url": "http://telus.com/fhir/StructureDefinition/ext-communication-priority",
"valueCode": "normal"
},
{
"url": "http://telus.com/fhir/StructureDefinition/ext-communication-topic",
"valueString": "dsads"
},
{
"url": "http://telus.com/fhir/StructureDefinition/ext-communication-state",
"valueCode": "draft"
},
{
"url": "http://telus.com/fhir/StructureDefinition/ext-communication-thread-id",
"valueString": "bd7bc833-953b-4379-b0b0-be3d898dee40"
}
],
"status": "in-progress",
"recipient": [
{
"reference": "RelatedPerson/17852"
}
],
"sender": {
"reference": "RelatedPerson/17427"
}
}

View File

@ -122,7 +122,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
*/
private String myServerVersion = createPoweredByHeaderProductVersion();
private boolean myStarted;
private Map<String, IResourceProvider> myTypeToProvider = new HashMap<>();
private boolean myUncompressIncomingContents = true;
private boolean myUseBrowserFriendlyContentTypes;
private ITenantIdentificationStrategy myTenantIdentificationStrategy;
@ -376,7 +375,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
try {
count += findResourceMethods(theProvider, clazz);
} catch (ConfigurationException e) {
throw new ConfigurationException("Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage());
throw new ConfigurationException("Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage(), e);
}
if (count == 0) {
throw new ConfigurationException("Did not find any annotated RESTful methods on provider class " + theProvider.getClass().getCanonicalName());
@ -1365,14 +1364,9 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
throw new NullPointerException("getResourceType() on class '" + rsrcProvider.getClass().getCanonicalName() + "' returned null");
}
String resourceName = getFhirContext().getResourceDefinition(resourceType).getName();
if (myTypeToProvider.containsKey(resourceName)) {
throw new ConfigurationException("Multiple resource providers return resource type[" + resourceName + "]: First[" + myTypeToProvider.get(resourceName).getClass().getCanonicalName()
+ "] and Second[" + rsrcProvider.getClass().getCanonicalName() + "]");
}
if (!inInit) {
myResourceProviders.add(rsrcProvider);
}
myTypeToProvider.put(resourceName, rsrcProvider);
providedResourceScanner.scanForProvidedResources(rsrcProvider);
newResourceProviders.add(rsrcProvider);
} else {
@ -1384,7 +1378,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
}
if (!newResourceProviders.isEmpty()) {
ourLog.info("Added {} resource provider(s). Total {}", newResourceProviders.size(), myTypeToProvider.size());
ourLog.info("Added {} resource provider(s). Total {}", newResourceProviders.size(), myResourceProviders.size());
for (IResourceProvider provider : newResourceProviders) {
assertProviderIsValid(provider);
findResourceMethods(provider);
@ -1594,7 +1588,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
IResourceProvider rsrcProvider = (IResourceProvider) provider;
Class<? extends IBaseResource> resourceType = rsrcProvider.getResourceType();
String resourceName = getFhirContext().getResourceDefinition(resourceType).getName();
myTypeToProvider.remove(resourceName);
providedResourceScanner.removeProvidedResources(rsrcProvider);
} else {
myPlainProviders.remove(provider);

View File

@ -20,18 +20,7 @@ package ca.uhn.fhir.rest.server.method;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Include;
@ -44,8 +33,22 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.binder.CollectionBinder;
import ca.uhn.fhir.rest.server.method.OperationParameter.IOperationParamConverter;
import ca.uhn.fhir.rest.server.method.ResourceParameter.Mode;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.ReflectionUtil;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class MethodUtil {
@ -90,7 +93,26 @@ public class MethodUtil {
}
if (Collection.class.isAssignableFrom(parameterType)) {
throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName()
+ "' is of an invalid generic type (can not be a collection of a collection of a collection)");
+ "' is of an invalid generic type (can not be a collection of a collection of a collection)");
}
/*
* If the user is trying to bind IPrimitiveType they are probably
* trying to write code that is compatible across versions of FHIR.
* We'll try and come up with an appropriate subtype to give
* them.
*
* This gets tested in HistoryR4Test
*/
if (IPrimitiveType.class.equals(parameterType)) {
Class<?> genericType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex);
if (Date.class.equals(genericType)) {
BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("dateTime");
parameterType = dateTimeDef.getImplementingClass();
} else if (String.class.equals(genericType) || genericType == null) {
BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("string");
parameterType = dateTimeDef.getImplementingClass();
}
}
}
@ -141,7 +163,7 @@ public class MethodUtil {
specType = String.class;
} else if ((parameterType != Include.class) || innerCollectionType == null || outerCollectionType != null) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<"
+ Include.class.getSimpleName() + ">");
+ Include.class.getSimpleName() + ">");
} else {
instantiableCollectionType = (Class<? extends Collection<Include>>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'");
specType = parameterType;
@ -198,7 +220,7 @@ public class MethodUtil {
} else if (nextAnnotation instanceof Validate.Mode) {
if (parameterType.equals(ValidationModeEnum.class) == false) {
throw new ConfigurationException(
"Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName());
"Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName());
}
param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IOperationParamConverter() {
@Override
@ -221,7 +243,7 @@ public class MethodUtil {
} else if (nextAnnotation instanceof Validate.Profile) {
if (parameterType.equals(String.class) == false) {
throw new ConfigurationException(
"Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName());
"Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName());
}
param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IOperationParamConverter() {
@Override
@ -244,8 +266,8 @@ public class MethodUtil {
if (param == null) {
throw new ConfigurationException(
"Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName()
+ "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter");
"Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName()
+ "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter");
}
param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType);

View File

@ -47,8 +47,8 @@ public class SearchParameter extends BaseQueryParameter {
static final String QUALIFIER_ANY_TYPE = ":*";
static {
ourParamTypes = new HashMap<Class<?>, RestSearchParameterTypeEnum>();
ourParamQualifiers = new HashMap<RestSearchParameterTypeEnum, Set<String>>();
ourParamTypes = new HashMap<>();
ourParamQualifiers = new HashMap<>();
ourParamTypes.put(StringParam.class, RestSearchParameterTypeEnum.STRING);
ourParamTypes.put(StringOrListParam.class, RestSearchParameterTypeEnum.STRING);
@ -124,7 +124,7 @@ public class SearchParameter extends BaseQueryParameter {
*/
@Override
public List<QualifiedParamList> encode(FhirContext theContext, Object theObject) throws InternalErrorException {
ArrayList<QualifiedParamList> retVal = new ArrayList<QualifiedParamList>();
ArrayList<QualifiedParamList> retVal = new ArrayList<>();
// TODO: declaring method should probably have a generic type..
@SuppressWarnings("rawtypes")
@ -197,7 +197,7 @@ public class SearchParameter extends BaseQueryParameter {
}
public void setChainlists(String[] theChainWhitelist, String[] theChainBlacklist) {
myQualifierWhitelist = new HashSet<String>(theChainWhitelist.length);
myQualifierWhitelist = new HashSet<>(theChainWhitelist.length);
myQualifierWhitelist.add(QUALIFIER_ANY_TYPE);
for (int i = 0; i < theChainWhitelist.length; i++) {
@ -211,7 +211,7 @@ public class SearchParameter extends BaseQueryParameter {
}
if (theChainBlacklist.length > 0) {
myQualifierBlacklist = new HashSet<String>(theChainBlacklist.length);
myQualifierBlacklist = new HashSet<>(theChainBlacklist.length);
for (String next : theChainBlacklist) {
if (next.equals(EMPTY_STRING)) {
myQualifierBlacklist.add(EMPTY_STRING);
@ -282,7 +282,7 @@ public class SearchParameter extends BaseQueryParameter {
Set<String> builtInQualifiers = ourParamQualifiers.get(typeEnum);
if (builtInQualifiers != null) {
if (myQualifierWhitelist != null) {
HashSet<String> qualifierWhitelist = new HashSet<String>();
HashSet<String> qualifierWhitelist = new HashSet<>();
qualifierWhitelist.addAll(myQualifierWhitelist);
qualifierWhitelist.addAll(builtInQualifiers);
myQualifierWhitelist = qualifierWhitelist;

View File

@ -20,7 +20,9 @@ package ca.uhn.fhir.rest.server.provider;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.param.TokenAndListParam;
@ -29,13 +31,16 @@ import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
/**
* This class is a simple implementation of the resource provider
@ -59,6 +64,8 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
private final FhirContext myFhirContext;
private final String myResourceName;
protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = new HashMap<>();
protected Map<String, LinkedList<T>> myIdToHistory = new HashMap<>();
protected LinkedList<T> myTypeHistory = new LinkedList<>();
private long myNextId;
private AtomicLong myDeleteCount = new AtomicLong(0);
private AtomicLong mySearchCount = new AtomicLong(0);
@ -86,6 +93,8 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
public void clear() {
myNextId = 1;
myIdToVersionToResourceMap.clear();
myIdToHistory.clear();
myTypeHistory.clear();
}
/**
@ -183,6 +192,21 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
return myIdToVersionToResourceMap.get(theIdPart);
}
@History
public List<T> historyInstance(@IdParam IIdType theId) {
LinkedList<T> retVal = myIdToHistory.get(theId.getIdPart());
if (retVal == null) {
throw new ResourceNotFoundException(theId);
}
return retVal;
}
@History
public List<T> historyType() {
return myTypeHistory;
}
@Read(version = true)
public IBaseResource read(@IdParam IIdType theId) {
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
@ -252,16 +276,52 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
private IIdType store(@ResourceParam T theResource, String theIdPart, Long theVersionIdPart) {
IIdType id = myFhirContext.getVersion().newIdType();
id.setParts(null, myResourceName, theIdPart, Long.toString(theVersionIdPart));
String versionIdPart = Long.toString(theVersionIdPart);
id.setParts(null, myResourceName, theIdPart, versionIdPart);
if (theResource != null) {
theResource.setId(id);
}
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
versionToResource.put(theVersionIdPart, theResource);
/*
* This is a bit of magic to make sure that the versionId attribute
* in the resource being stored accurately represents the version
* that was assigned by this provider
*/
if (theResource != null) {
if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) {
ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, versionIdPart);
} else {
BaseRuntimeChildDefinition metaChild = myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta");
List<IBase> metaValues = metaChild.getAccessor().getValues(theResource);
if (metaValues.size() > 0) {
IBase meta = metaValues.get(0);
BaseRuntimeElementCompositeDefinition<?> metaDef = (BaseRuntimeElementCompositeDefinition<?>) myFhirContext.getElementDefinition(meta.getClass());
BaseRuntimeChildDefinition versionIdDef = metaDef.getChildByName("versionId");
List<IBase> versionIdValues = versionIdDef.getAccessor().getValues(meta);
if (versionIdValues.size() > 0) {
IPrimitiveType<?> versionId = (IPrimitiveType<?>) versionIdValues.get(0);
versionId.setValueAsString(versionIdPart);
}
}
}
}
ourLog.info("Storing resource with ID: {}", id.getValue());
// Store to ID->version->resource map
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
versionToResource.put(theVersionIdPart, theResource);
// Store to type history map
myTypeHistory.addFirst(theResource);
// Store to ID history map
if (!myIdToHistory.containsKey(theIdPart)) {
myIdToHistory.put(theIdPart, new LinkedList<>());
}
myIdToHistory.get(theIdPart).addFirst(theResource);
// Return the newly assigned ID including the version ID
return id;
}

View File

@ -230,12 +230,14 @@ public class CreateR4Test {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
PatientProvider patientProvider = new PatientProvider();
PatientProviderCreate patientProviderCreate = new PatientProviderCreate();
PatientProviderRead patientProviderRead = new PatientProviderRead();
PatientProviderSearch patientProviderSearch = new PatientProviderSearch();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setResourceProviders(patientProvider);
servlet.setResourceProviders(patientProviderCreate, patientProviderRead, patientProviderSearch);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
@ -247,19 +249,7 @@ public class CreateR4Test {
ourClient = builder.build();
}
public static class PatientProvider implements IResourceProvider {
@Create()
public MethodOutcome create(@ResourceParam Patient theIdParam) {
assertNull(theIdParam.getIdElement().getIdPart());
return new MethodOutcome(new IdType("Patient", "1"), true).setOperationOutcome(ourReturnOo);
}
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
public static class PatientProviderRead implements IResourceProvider {
@Read()
public MyPatientWithExtensions read(@IdParam IdType theIdParam) {
@ -269,6 +259,33 @@ public class CreateR4Test {
return p0;
}
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
}
public static class PatientProviderCreate implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Create()
public MethodOutcome create(@ResourceParam Patient theIdParam) {
assertNull(theIdParam.getIdElement().getIdPart());
return new MethodOutcome(new IdType("Patient", "1"), true).setOperationOutcome(ourReturnOo);
}
}
public static class PatientProviderSearch implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Search
public List<IBaseResource> search() {
ArrayList<IBaseResource> retVal = new ArrayList<IBaseResource>();

View File

@ -1,13 +1,14 @@
package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
@ -15,65 +16,53 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.*;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.annotation.At;
import ca.uhn.fhir.rest.annotation.History;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.Since;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class HistoryDstu2Test {
import static org.junit.Assert.*;
public class HistoryR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(HistoryR4Test.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu2();
private static FhirContext ourCtx = FhirContext.forR4();
private static DateRangeParam ourLastAt;
private static InstantDt ourLastSince;
private static InstantType ourLastSince;
private static IPrimitiveType<Date> ourLastSince2;
private static IPrimitiveType<String> ourLastSince3;
private static IPrimitiveType<?> ourLastSince4;
private static int ourPort;
private static Server ourServer;
@Before
public void before() {
ourLastAt = null;
ourLastSince = null;
}
@Test
public void testSince() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history?_since=2005");
HttpResponse status = ourClient.execute(httpGet);
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals(null, ourLastAt);
assertEquals("2005", ourLastSince.getValueAsString());
}
ourLastSince2 = null;
ourLastSince3 = null;
ourLastSince4 = null;
}
@Test
public void testAt() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history?_at=gt2001&_at=lt2005");
HttpResponse status = ourClient.execute(httpGet);
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
assertEquals(ParamPrefixEnum.GREATERTHAN, ourLastAt.getLowerBound().getPrefix());
assertEquals("2001", ourLastAt.getLowerBound().getValueAsString());
@ -86,56 +75,79 @@ public class HistoryDstu2Test {
public void testInstanceHistory() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/_history?_pretty=true");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent);
assertEquals(2, bundle.getEntry().size());
assertEquals("http://localhost:" + ourPort + "/Patient/ih1/_history/1", bundle.getEntry().get(0).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/ih1/_history/2", bundle.getEntry().get(1).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/ih1/_history/1", bundle.getEntry().get(0).getResource().getId());
assertEquals("http://localhost:" + ourPort + "/Patient/ih1/_history/2", bundle.getEntry().get(1).getResource().getId());
}
}private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HistoryDstu2Test.class);
}
@Test
public void testServerHistory() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent);
assertEquals(2, bundle.getEntry().size());
assertEquals("http://localhost:" + ourPort + "/Patient/h1/_history/1", bundle.getEntry().get(0).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/h1/_history/2", bundle.getEntry().get(1).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/h1/_history/1", bundle.getEntry().get(0).getResource().getId());
assertEquals("http://localhost:" + ourPort + "/Patient/h1/_history/2", bundle.getEntry().get(1).getResource().getId());
}
}
@Test
public void testSince() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/_history?_since=2005");
String responseContent;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
assertEquals(null, ourLastAt);
assertEquals("2005", ourLastSince.getValueAsString());
assertEquals("2005", ourLastSince2.getValueAsString());
assertTrue(DateTimeType.class.equals(ourLastSince2.getClass()));
assertEquals("2005", ourLastSince3.getValueAsString());
assertTrue(StringType.class.equals(ourLastSince3.getClass()));
assertEquals("2005", ourLastSince4.getValueAsString());
assertTrue(StringType.class.equals(ourLastSince4.getClass()));
}
}
@Test
public void testTypeHistory() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/_history");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
assertNull(ourLastAt);
Bundle bundle = ourCtx.newXmlParser().parseResource(Bundle.class, responseContent);
assertEquals(2, bundle.getEntry().size());
assertEquals("http://localhost:" + ourPort + "/Patient/th1/_history/1", bundle.getEntry().get(0).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/th1/_history/2", bundle.getEntry().get(1).getResource().getId().getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/th1/_history/1", bundle.getEntry().get(0).getResource().getId());
assertEquals("http://localhost:" + ourPort + "/Patient/th1/_history/2", bundle.getEntry().get(1).getResource().getId());
}
}
@ -147,14 +159,15 @@ public class HistoryDstu2Test {
public void testVread() throws Exception {
{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/_history/456");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
}
Patient bundle = ourCtx.newXmlParser().parseResource(Patient.class, responseContent);
assertEquals("vread", bundle.getNameFirstRep().getFamilyFirstRep().getValue());
assertEquals("vread", bundle.getNameFirstRep().getFamily());
}
}
@ -191,20 +204,27 @@ public class HistoryDstu2Test {
public static class DummyPlainProvider {
@History
public List<Patient> history(@Since InstantDt theSince, @At DateRangeParam theAt) {
public List<Patient> history(@Since InstantType theSince,
@Since IPrimitiveType<Date> theSince2,
@Since IPrimitiveType<String> theSince3,
@Since IPrimitiveType theSince4,
@At DateRangeParam theAt) {
ourLastAt = theAt;
ourLastSince = theSince;
ourLastSince2 = theSince2;
ourLastSince3 = theSince3;
ourLastSince4 = theSince4;
ArrayList<Patient> retVal = new ArrayList<Patient>();
ArrayList<Patient> retVal = new ArrayList<>();
Patient patient = new Patient();
patient.setId("Patient/h1/_history/1");
patient.addName().addFamily("history");
patient.addName().setFamily("history");
retVal.add(patient);
Patient patient2 = new Patient();
patient2.setId("Patient/h1/_history/2");
patient2.addName().addFamily("history");
patient2.addName().setFamily("history");
retVal.add(patient2);
return retVal;
@ -220,17 +240,17 @@ public class HistoryDstu2Test {
}
@History
public List<Patient> instanceHistory(@IdParam IdDt theId) {
ArrayList<Patient> retVal = new ArrayList<Patient>();
public List<Patient> instanceHistory(@IdParam IdType theId) {
ArrayList<Patient> retVal = new ArrayList<>();
Patient patient = new Patient();
patient.setId("Patient/ih1/_history/1");
patient.addName().addFamily("history");
patient.addName().setFamily("history");
retVal.add(patient);
Patient patient2 = new Patient();
patient2.setId("Patient/ih1/_history/2");
patient2.addName().addFamily("history");
patient2.addName().setFamily("history");
retVal.add(patient2);
return retVal;
@ -238,25 +258,25 @@ public class HistoryDstu2Test {
@History
public List<Patient> typeHistory() {
ArrayList<Patient> retVal = new ArrayList<Patient>();
ArrayList<Patient> retVal = new ArrayList<>();
Patient patient = new Patient();
patient.setId("Patient/th1/_history/1");
patient.addName().addFamily("history");
patient.addName().setFamily("history");
retVal.add(patient);
Patient patient2 = new Patient();
patient2.setId("Patient/th1/_history/2");
patient2.addName().addFamily("history");
patient2.addName().setFamily("history");
retVal.add(patient2);
return retVal;
}
@Read(version = true)
public Patient vread(@IdParam IdDt theId) {
public Patient vread(@IdParam IdType theId) {
Patient retVal = new Patient();
retVal.addName().addFamily("vread");
retVal.addName().setFamily("vread");
retVal.setId(theId);
return retVal;
}

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.server.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
@ -19,17 +20,19 @@ import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
public class HashMapResourceProviderTest {
private static final Logger ourLog = LoggerFactory.getLogger(HashMapResourceProviderTest.class);
private static MyRestfulServer ourRestServer;
private static Server ourListenerServer;
private static IGenericClient ourClient;
@ -100,6 +103,93 @@ public class HashMapResourceProviderTest {
}
}
@Test
public void testHistoryInstance() {
// Create Res 1
Patient p = new Patient();
p.setActive(true);
IIdType id1 = ourClient.create().resource(p).execute().getId();
assertThat(id1.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id1.getVersionIdPart());
// Create Res 2
p = new Patient();
p.setActive(true);
IIdType id2 = ourClient.create().resource(p).execute().getId();
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id2.getVersionIdPart());
// Update Res 2
p = new Patient();
p.setId(id2);
p.setActive(false);
id2 = ourClient.update().resource(p).execute().getId();
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("2", id2.getVersionIdPart());
Bundle history = ourClient
.history()
.onInstance(id2.toUnqualifiedVersionless())
.andReturnBundle(Bundle.class)
.encodedJson()
.prettyPrint()
.execute();
ourLog.debug(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(history));
List<String> ids = history
.getEntry()
.stream()
.map(t -> t.getResource().getIdElement().toUnqualified().getValue())
.collect(Collectors.toList());
assertThat(ids, contains(
id2.toUnqualified().withVersion("2").getValue(),
id2.toUnqualified().withVersion("1").getValue()
));
}
@Test
public void testHistoryType() {
// Create Res 1
Patient p = new Patient();
p.setActive(true);
IIdType id1 = ourClient.create().resource(p).execute().getId();
assertThat(id1.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id1.getVersionIdPart());
// Create Res 2
p = new Patient();
p.setActive(true);
IIdType id2 = ourClient.create().resource(p).execute().getId();
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("1", id2.getVersionIdPart());
// Update Res 2
p = new Patient();
p.setId(id2);
p.setActive(false);
id2 = ourClient.update().resource(p).execute().getId();
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
assertEquals("2", id2.getVersionIdPart());
Bundle history = ourClient
.history()
.onType(Patient.class)
.andReturnBundle(Bundle.class)
.execute();
List<String> ids = history
.getEntry()
.stream()
.map(t -> t.getResource().getIdElement().toUnqualified().getValue())
.collect(Collectors.toList());
ourLog.info("Received IDs: {}", ids);
assertThat(ids, contains(
id2.toUnqualified().withVersion("2").getValue(),
id2.toUnqualified().withVersion("1").getValue(),
id1.toUnqualified().withVersion("1").getValue()
));
}
@Test
public void testSearchAll() {
// Create

View File

@ -86,7 +86,7 @@
Resource loading logic for the JPA server has been optimized to
reduce the number of database round trips required when loading
search results where many of the entries have a "forced ID" (an alphanumeric
client-assigned reosurce ID). Thanks to Frank Tao for the pull
client-assigned resource ID). Thanks to Frank Tao for the pull
request!
</action>
<action type="add">
@ -150,6 +150,39 @@
in order to prevent HTML injection attacks via maliciously
crafted URLs.
</action>
<action type="add">
The REST Server module now allows more than one Resource Provider
(i.e. more than one implementation of IResourceProvider) to be registered
to the RestfulServer for the same resource type. Previous versions of
HAPI FHIR have always limited support to a single resource provider, but
this limitation did not serve any purpose so it has been removed.
</action>
<action type="add">
The HashMapResourceProvider now supports the type and
instance history operations.
</action>
<action type="fix">
Fixed a bug when creating a custom search parameter in the JPA
server: if the SearchParameter resource contained an invalid
expression, create/update operations for the given resource would
fail with a cryptic error. SearchParameter expressions are now
validated upon storage, and the SearchParameter will be rejected
if the expression can not be processed.
</action>
<action type="add">
The generic client history operations (history-instance, history-type,
and history-server) now support the
<![CDATA[<code>_at</code>]]> parameter.
</action>
<action type="add">
In the plain server, many resource provider method parameters may now
use a generic
<![CDATA[<code>IPrimitiveType&lt;String&gt;</code>]]>
or
<![CDATA[<code>IPrimitiveType&lt;Date&gt;</code>]]> at the
parameter type. This is handy if you are trying to write code
that works across versions of FHIR.
</action>
</release>
<release version="3.4.0" date="2018-05-28">
<action type="add">