merging in master

This commit is contained in:
leif stawnyczy 2024-11-08 10:58:49 -05:00
commit 78e4eee683
110 changed files with 3711 additions and 1067 deletions

View File

@ -440,74 +440,259 @@ public interface IValidationSupport {
return "Unknown " + getFhirContext().getVersion().getVersion() + " Validation Support";
}
/**
* Defines codes in system <a href="http://hl7.org/fhir/issue-severity">http://hl7.org/fhir/issue-severity</a>.
*/
/* this enum would not be needed if we design/refactor to use org.hl7.fhir.r5.terminologies.utilities.ValidationResult */
enum IssueSeverity {
/**
* The issue caused the action to fail, and no further checking could be performed.
*/
FATAL,
FATAL("fatal"),
/**
* The issue is sufficiently important to cause the action to fail.
*/
ERROR,
ERROR("error"),
/**
* The issue is not important enough to cause the action to fail, but may cause it to be performed suboptimally or in a way that is not as desired.
*/
WARNING,
WARNING("warning"),
/**
* The issue has no relation to the degree of success of the action.
*/
INFORMATION
INFORMATION("information"),
/**
* The operation was successful.
*/
SUCCESS("success");
// the spec for OperationOutcome mentions that a code from http://hl7.org/fhir/issue-severity is required
private final String myCode;
IssueSeverity(String theCode) {
myCode = theCode;
}
/**
* Provide mapping to a code in system <a href="http://hl7.org/fhir/issue-severity">http://hl7.org/fhir/issue-severity</a>.
* @return the code
*/
public String getCode() {
return myCode;
}
/**
* Creates a {@link IssueSeverity} object from the given code.
* @return the {@link IssueSeverity}
*/
public static IssueSeverity fromCode(String theCode) {
switch (theCode) {
case "fatal":
return FATAL;
case "error":
return ERROR;
case "warning":
return WARNING;
case "information":
return INFORMATION;
case "success":
return SUCCESS;
default:
return null;
}
}
}
enum CodeValidationIssueCode {
NOT_FOUND,
CODE_INVALID,
INVALID,
OTHER
}
/**
* Defines codes in system <a href="http://hl7.org/fhir/issue-type">http://hl7.org/fhir/issue-type</a>.
* The binding is enforced as a part of validation logic in the FHIR Core Validation library where an exception is thrown.
* Only a sub-set of these codes are defined as constants because they relate to validation,
* If there are additional ones that come up, for Remote Terminology they are currently supported via
* {@link IValidationSupport.CodeValidationIssue#CodeValidationIssue(String, IssueSeverity, String)}
* while for internal validators, more constants can be added to make things easier and consistent.
* This maps to resource OperationOutcome.issue.code.
*/
/* this enum would not be needed if we design/refactor to use org.hl7.fhir.r5.terminologies.utilities.ValidationResult */
class CodeValidationIssueCode {
public static final CodeValidationIssueCode NOT_FOUND = new CodeValidationIssueCode("not-found");
public static final CodeValidationIssueCode CODE_INVALID = new CodeValidationIssueCode("code-invalid");
public static final CodeValidationIssueCode INVALID = new CodeValidationIssueCode("invalid");
enum CodeValidationIssueCoding {
VS_INVALID,
NOT_FOUND,
NOT_IN_VS,
private final String myCode;
INVALID_CODE,
INVALID_DISPLAY,
OTHER
}
class CodeValidationIssue {
private final String myMessage;
private final IssueSeverity mySeverity;
private final CodeValidationIssueCode myCode;
private final CodeValidationIssueCoding myCoding;
public CodeValidationIssue(
String theMessage,
IssueSeverity mySeverity,
CodeValidationIssueCode theCode,
CodeValidationIssueCoding theCoding) {
this.myMessage = theMessage;
this.mySeverity = mySeverity;
this.myCode = theCode;
this.myCoding = theCoding;
// this is intentionally not exposed
CodeValidationIssueCode(String theCode) {
myCode = theCode;
}
/**
* Retrieve the corresponding code from system <a href="http://hl7.org/fhir/issue-type">http://hl7.org/fhir/issue-type</a>.
* @return the code
*/
public String getCode() {
return myCode;
}
}
/**
* Holds information about the details of a {@link CodeValidationIssue}.
* This maps to resource OperationOutcome.issue.details.
*/
/* this enum would not be needed if we design/refactor to use org.hl7.fhir.r5.terminologies.utilities.ValidationResult */
class CodeValidationIssueDetails {
private final String myText;
private List<CodeValidationIssueCoding> myCodings;
public CodeValidationIssueDetails(String theText) {
myText = theText;
}
// intentionally not exposed
void addCoding(CodeValidationIssueCoding theCoding) {
getCodings().add(theCoding);
}
public CodeValidationIssueDetails addCoding(String theSystem, String theCode) {
if (myCodings == null) {
myCodings = new ArrayList<>();
}
myCodings.add(new CodeValidationIssueCoding(theSystem, theCode));
return this;
}
public String getText() {
return myText;
}
public List<CodeValidationIssueCoding> getCodings() {
if (myCodings == null) {
myCodings = new ArrayList<>();
}
return myCodings;
}
}
/**
* Defines codes that can be part of the details of an issue.
* There are some constants available (pre-defined) for codes for system <a href="http://hl7.org/fhir/tools/CodeSystem/tx-issue-type">http://hl7.org/fhir/tools/CodeSystem/tx-issue-type</a>.
* This maps to resource OperationOutcome.issue.details.coding[0].code.
*/
class CodeValidationIssueCoding {
public static String TX_ISSUE_SYSTEM = "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type";
public static CodeValidationIssueCoding VS_INVALID =
new CodeValidationIssueCoding(TX_ISSUE_SYSTEM, "vs-invalid");
public static final CodeValidationIssueCoding NOT_FOUND =
new CodeValidationIssueCoding(TX_ISSUE_SYSTEM, "not-found");
public static final CodeValidationIssueCoding NOT_IN_VS =
new CodeValidationIssueCoding(TX_ISSUE_SYSTEM, "not-in-vs");
public static final CodeValidationIssueCoding INVALID_CODE =
new CodeValidationIssueCoding(TX_ISSUE_SYSTEM, "invalid-code");
public static final CodeValidationIssueCoding INVALID_DISPLAY =
new CodeValidationIssueCoding(TX_ISSUE_SYSTEM, "vs-display");
private final String mySystem, myCode;
// this is intentionally not exposed
CodeValidationIssueCoding(String theSystem, String theCode) {
mySystem = theSystem;
myCode = theCode;
}
/**
* Retrieve the corresponding code for the details of a validation issue.
* @return the code
*/
public String getCode() {
return myCode;
}
/**
* Retrieve the system for the details of a validation issue.
* @return the system
*/
public String getSystem() {
return mySystem;
}
}
/**
* This is a hapi-fhir internal version agnostic object holding information about a validation issue.
* An alternative (which requires significant refactoring) would be to use org.hl7.fhir.r5.terminologies.utilities.ValidationResult instead.
*/
class CodeValidationIssue {
private final String myDiagnostics;
private final IssueSeverity mySeverity;
private final CodeValidationIssueCode myCode;
private CodeValidationIssueDetails myDetails;
public CodeValidationIssue(
String theDiagnostics, IssueSeverity theSeverity, CodeValidationIssueCode theTypeCode) {
this(theDiagnostics, theSeverity, theTypeCode, null);
}
public CodeValidationIssue(String theDiagnostics, IssueSeverity theSeverity, String theTypeCode) {
this(theDiagnostics, theSeverity, new CodeValidationIssueCode(theTypeCode), null);
}
public CodeValidationIssue(
String theDiagnostics,
IssueSeverity theSeverity,
CodeValidationIssueCode theType,
CodeValidationIssueCoding theDetailsCoding) {
myDiagnostics = theDiagnostics;
mySeverity = theSeverity;
myCode = theType;
// reuse the diagnostics message as a detail text message
myDetails = new CodeValidationIssueDetails(theDiagnostics);
myDetails.addCoding(theDetailsCoding);
}
/**
* @deprecated Please use {@link #getDiagnostics()} instead.
*/
@Deprecated(since = "7.4.6")
public String getMessage() {
return myMessage;
return getDiagnostics();
}
public String getDiagnostics() {
return myDiagnostics;
}
public IssueSeverity getSeverity() {
return mySeverity;
}
/**
* @deprecated Please use {@link #getType()} instead.
*/
@Deprecated(since = "7.4.6")
public CodeValidationIssueCode getCode() {
return getType();
}
public CodeValidationIssueCode getType() {
return myCode;
}
/**
* @deprecated Please use {@link #getDetails()} instead. That has support for multiple codings.
*/
@Deprecated(since = "7.4.6")
public CodeValidationIssueCoding getCoding() {
return myCoding;
return myDetails != null
? myDetails.getCodings().stream().findFirst().orElse(null)
: null;
}
public void setDetails(CodeValidationIssueDetails theDetails) {
this.myDetails = theDetails;
}
public CodeValidationIssueDetails getDetails() {
return myDetails;
}
public boolean hasIssueDetailCode(@Nonnull String theCode) {
// this method is system agnostic at the moment but it can be restricted if needed
return myDetails.getCodings().stream().anyMatch(coding -> theCode.equals(coding.getCode()));
}
}
@ -671,6 +856,10 @@ public interface IValidationSupport {
}
}
/**
* This is a hapi-fhir internal version agnostic object holding information about the validation result.
* An alternative (which requires significant refactoring) would be to use org.hl7.fhir.r5.terminologies.utilities.ValidationResult.
*/
class CodeValidationResult {
public static final String SOURCE_DETAILS = "sourceDetails";
public static final String RESULT = "result";
@ -686,7 +875,7 @@ public interface IValidationSupport {
private String myDisplay;
private String mySourceDetails;
private List<CodeValidationIssue> myCodeValidationIssues;
private List<CodeValidationIssue> myIssues;
public CodeValidationResult() {
super();
@ -771,20 +960,45 @@ public interface IValidationSupport {
return this;
}
/**
* @deprecated Please use method {@link #getIssues()} instead.
*/
@Deprecated(since = "7.4.6")
public List<CodeValidationIssue> getCodeValidationIssues() {
if (myCodeValidationIssues == null) {
myCodeValidationIssues = new ArrayList<>();
}
return myCodeValidationIssues;
return getIssues();
}
/**
* @deprecated Please use method {@link #setIssues(List)} instead.
*/
@Deprecated(since = "7.4.6")
public CodeValidationResult setCodeValidationIssues(List<CodeValidationIssue> theCodeValidationIssues) {
myCodeValidationIssues = new ArrayList<>(theCodeValidationIssues);
return setIssues(theCodeValidationIssues);
}
/**
* @deprecated Please use method {@link #addIssue(CodeValidationIssue)} instead.
*/
@Deprecated(since = "7.4.6")
public CodeValidationResult addCodeValidationIssue(CodeValidationIssue theCodeValidationIssue) {
getCodeValidationIssues().add(theCodeValidationIssue);
return this;
}
public CodeValidationResult addCodeValidationIssue(CodeValidationIssue theCodeValidationIssue) {
getCodeValidationIssues().add(theCodeValidationIssue);
public List<CodeValidationIssue> getIssues() {
if (myIssues == null) {
myIssues = new ArrayList<>();
}
return myIssues;
}
public CodeValidationResult setIssues(List<CodeValidationIssue> theIssues) {
myIssues = new ArrayList<>(theIssues);
return this;
}
public CodeValidationResult addIssue(CodeValidationIssue theCodeValidationIssue) {
getIssues().add(theCodeValidationIssue);
return this;
}
@ -811,17 +1025,19 @@ public interface IValidationSupport {
public String getSeverityCode() {
String retVal = null;
if (getSeverity() != null) {
retVal = getSeverity().name().toLowerCase();
retVal = getSeverity().getCode();
}
return retVal;
}
/**
* Sets an issue severity as a string code. Value must be the name of
* one of the enum values in {@link IssueSeverity}. Value is case-insensitive.
* Sets an issue severity using a severity code. Please use method {@link #setSeverity(IssueSeverity)} instead.
* @param theSeverityCode the code
* @return the current {@link CodeValidationResult} instance
*/
public CodeValidationResult setSeverityCode(@Nonnull String theIssueSeverity) {
setSeverity(IssueSeverity.valueOf(theIssueSeverity.toUpperCase()));
@Deprecated(since = "7.4.6")
public CodeValidationResult setSeverityCode(@Nonnull String theSeverityCode) {
setSeverity(IssueSeverity.fromCode(theSeverityCode));
return this;
}
@ -838,6 +1054,11 @@ public interface IValidationSupport {
if (isNotBlank(getSourceDetails())) {
ParametersUtil.addParameterToParametersString(theContext, retVal, SOURCE_DETAILS, getSourceDetails());
}
/*
should translate issues as well, except that is version specific code, so it requires more refactoring
or replace the current class with org.hl7.fhir.r5.terminologies.utilities.ValidationResult
@see VersionSpecificWorkerContextWrapper#getIssuesForCodeValidation
*/
return retVal;
}

View File

@ -105,7 +105,6 @@ public abstract class BaseParser implements IParser {
private static final Set<String> notEncodeForContainedResource =
new HashSet<>(Arrays.asList("security", "versionId", "lastUpdated"));
private FhirTerser.ContainedResources myContainedResources;
private boolean myEncodeElementsAppliesToChildResourcesOnly;
private final FhirContext myContext;
private Collection<String> myDontEncodeElements;
@ -183,12 +182,15 @@ public abstract class BaseParser implements IParser {
}
private String determineReferenceText(
IBaseReference theRef, CompositeChildElement theCompositeChildElement, IBaseResource theResource) {
IBaseReference theRef,
CompositeChildElement theCompositeChildElement,
IBaseResource theResource,
EncodeContext theContext) {
IIdType ref = theRef.getReferenceElement();
if (isBlank(ref.getIdPart())) {
String reference = ref.getValue();
if (theRef.getResource() != null) {
IIdType containedId = getContainedResources().getResourceId(theRef.getResource());
IIdType containedId = theContext.getContainedResources().getResourceId(theRef.getResource());
if (containedId != null && !containedId.isEmpty()) {
if (containedId.isLocal()) {
reference = containedId.getValue();
@ -262,7 +264,8 @@ public abstract class BaseParser implements IParser {
@Override
public final void encodeResourceToWriter(IBaseResource theResource, Writer theWriter)
throws IOException, DataFormatException {
EncodeContext encodeContext = new EncodeContext(this, myContext.getParserOptions());
EncodeContext encodeContext =
new EncodeContext(this, myContext.getParserOptions(), new FhirTerser.ContainedResources());
encodeResourceToWriter(theResource, theWriter, encodeContext);
}
@ -285,7 +288,8 @@ public abstract class BaseParser implements IParser {
} else if (theElement instanceof IPrimitiveType) {
theWriter.write(((IPrimitiveType<?>) theElement).getValueAsString());
} else {
EncodeContext encodeContext = new EncodeContext(this, myContext.getParserOptions());
EncodeContext encodeContext =
new EncodeContext(this, myContext.getParserOptions(), new FhirTerser.ContainedResources());
encodeToWriter(theElement, theWriter, encodeContext);
}
}
@ -404,10 +408,6 @@ public abstract class BaseParser implements IParser {
return elementId;
}
FhirTerser.ContainedResources getContainedResources() {
return myContainedResources;
}
@Override
public Set<String> getDontStripVersionsFromReferencesAtPaths() {
return myDontStripVersionsFromReferencesAtPaths;
@ -539,10 +539,11 @@ public abstract class BaseParser implements IParser {
return mySuppressNarratives;
}
protected boolean isChildContained(BaseRuntimeElementDefinition<?> childDef, boolean theIncludedResource) {
protected boolean isChildContained(
BaseRuntimeElementDefinition<?> childDef, boolean theIncludedResource, EncodeContext theContext) {
return (childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCES
|| childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST)
&& getContainedResources().isEmpty() == false
&& theContext.getContainedResources().isEmpty() == false
&& theIncludedResource == false;
}
@ -788,7 +789,8 @@ public abstract class BaseParser implements IParser {
*/
if (next instanceof IBaseReference) {
IBaseReference nextRef = (IBaseReference) next;
String refText = determineReferenceText(nextRef, theCompositeChildElement, theResource);
String refText =
determineReferenceText(nextRef, theCompositeChildElement, theResource, theEncodeContext);
if (!StringUtils.equals(refText, nextRef.getReferenceElement().getValue())) {
if (retVal == theValues) {
@ -980,7 +982,7 @@ public abstract class BaseParser implements IParser {
return true;
}
protected void containResourcesInReferences(IBaseResource theResource) {
protected void containResourcesInReferences(IBaseResource theResource, EncodeContext theContext) {
/*
* If a UUID is present in Bundle.entry.fullUrl but no value is present
@ -1003,7 +1005,7 @@ public abstract class BaseParser implements IParser {
}
}
myContainedResources = getContext().newTerser().containResources(theResource);
theContext.setContainedResources(getContext().newTerser().containResources(theResource));
}
static class ChildNameAndDef {
@ -1034,8 +1036,12 @@ public abstract class BaseParser implements IParser {
private final List<EncodeContextPath> myEncodeElementPaths;
private final Set<String> myEncodeElementsAppliesToResourceTypes;
private final List<EncodeContextPath> myDontEncodeElementPaths;
private FhirTerser.ContainedResources myContainedResources;
public EncodeContext(BaseParser theParser, ParserOptions theParserOptions) {
public EncodeContext(
BaseParser theParser,
ParserOptions theParserOptions,
FhirTerser.ContainedResources theContainedResources) {
Collection<String> encodeElements = theParser.myEncodeElements;
Collection<String> dontEncodeElements = theParser.myDontEncodeElements;
if (isSummaryMode()) {
@ -1058,6 +1064,8 @@ public abstract class BaseParser implements IParser {
dontEncodeElements.stream().map(EncodeContextPath::new).collect(Collectors.toList());
}
myContainedResources = theContainedResources;
myEncodeElementsAppliesToResourceTypes =
ParserUtil.determineApplicableResourceTypesForTerserPaths(myEncodeElementPaths);
}
@ -1065,6 +1073,14 @@ public abstract class BaseParser implements IParser {
private Map<Key, List<BaseParser.CompositeChildElement>> getCompositeChildrenCache() {
return myCompositeChildrenCache;
}
public FhirTerser.ContainedResources getContainedResources() {
return myContainedResources;
}
public void setContainedResources(FhirTerser.ContainedResources theContainedResources) {
myContainedResources = theContainedResources;
}
}
protected class CompositeChildElement {

View File

@ -54,6 +54,7 @@ import ca.uhn.fhir.parser.json.JsonLikeStructure;
import ca.uhn.fhir.parser.json.jackson.JacksonStructure;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.util.ElementUtil;
import ca.uhn.fhir.util.FhirTerser;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.text.WordUtils;
@ -386,12 +387,14 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
}
case CONTAINED_RESOURCE_LIST:
case CONTAINED_RESOURCES: {
List<IBaseResource> containedResources = getContainedResources().getContainedResources();
List<IBaseResource> containedResources =
theEncodeContext.getContainedResources().getContainedResources();
if (containedResources.size() > 0) {
beginArray(theEventWriter, theChildName);
for (IBaseResource next : containedResources) {
IIdType resourceId = getContainedResources().getResourceId(next);
IIdType resourceId =
theEncodeContext.getContainedResources().getResourceId(next);
String value = resourceId.getValue();
encodeResourceToJsonStreamWriter(
theResDef,
@ -554,7 +557,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
if (nextValue == null || nextValue.isEmpty()) {
if (nextValue instanceof BaseContainedDt) {
if (theContainedResource || getContainedResources().isEmpty()) {
if (theContainedResource
|| theEncodeContext.getContainedResources().isEmpty()) {
continue;
}
} else {
@ -838,7 +842,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
+ theResource.getStructureFhirVersionEnum());
}
EncodeContext encodeContext = new EncodeContext(this, getContext().getParserOptions());
EncodeContext encodeContext =
new EncodeContext(this, getContext().getParserOptions(), new FhirTerser.ContainedResources());
String resourceName = getContext().getResourceType(theResource);
encodeContext.pushPath(resourceName, true);
doEncodeResourceToJsonLikeWriter(theResource, theJsonLikeWriter, encodeContext);
@ -894,7 +899,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
}
if (!theContainedResource) {
containResourcesInReferences(theResource);
containResourcesInReferences(theResource, theEncodeContext);
}
RuntimeResourceDefinition resDef = getContext().getResourceDefinition(theResource);

View File

@ -191,7 +191,7 @@ public class RDFParser extends BaseParser {
}
if (!containedResource) {
containResourcesInReferences(resource);
containResourcesInReferences(resource, encodeContext);
}
if (!(resource instanceof IAnyResource)) {
@ -354,7 +354,7 @@ public class RDFParser extends BaseParser {
try {
if (element == null || element.isEmpty()) {
if (!isChildContained(childDef, includedResource)) {
if (!isChildContained(childDef, includedResource, theEncodeContext)) {
return rdfModel;
}
}

View File

@ -295,7 +295,7 @@ public class XmlParser extends BaseParser {
try {
if (theElement == null || theElement.isEmpty()) {
if (isChildContained(childDef, theIncludedResource)) {
if (isChildContained(childDef, theIncludedResource, theEncodeContext)) {
// We still want to go in..
} else {
return;
@ -359,8 +359,10 @@ public class XmlParser extends BaseParser {
* theEventWriter.writeStartElement("contained"); encodeResourceToXmlStreamWriter(next, theEventWriter, true, fixContainedResourceId(next.getId().getValue()));
* theEventWriter.writeEndElement(); }
*/
for (IBaseResource next : getContainedResources().getContainedResources()) {
IIdType resourceId = getContainedResources().getResourceId(next);
for (IBaseResource next :
theEncodeContext.getContainedResources().getContainedResources()) {
IIdType resourceId =
theEncodeContext.getContainedResources().getResourceId(next);
theEventWriter.writeStartElement("contained");
String value = resourceId.getValue();
encodeResourceToXmlStreamWriter(
@ -682,7 +684,7 @@ public class XmlParser extends BaseParser {
}
if (!theContainedResource) {
containResourcesInReferences(theResource);
containResourcesInReferences(theResource, theEncodeContext);
}
theEventWriter.writeStartElement(resDef.getName());

View File

@ -28,6 +28,8 @@ import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
import com.google.common.annotations.Beta;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseParameters;
@ -231,6 +233,23 @@ public interface Repository {
// Querying starts here
/**
* Searches this repository
*
* @see <a href="https://www.hl7.org/fhir/http.html#search">FHIR search</a>
*
* @param <B> a Bundle type
* @param <T> a Resource type
* @param bundleType the class of the Bundle type to return
* @param resourceType the class of the Resource type to search
* @param searchParameters the searchParameters for this search
* @return a Bundle with the results of the search
*/
default <B extends IBaseBundle, T extends IBaseResource> B search(
Class<B> bundleType, Class<T> resourceType, Multimap<String, List<IQueryParameterType>> searchParameters) {
return this.search(bundleType, resourceType, searchParameters, Collections.emptyMap());
}
/**
* Searches this repository
*
@ -264,9 +283,32 @@ public interface Repository {
<B extends IBaseBundle, T extends IBaseResource> B search(
Class<B> bundleType,
Class<T> resourceType,
Map<String, List<IQueryParameterType>> searchParameters,
Multimap<String, List<IQueryParameterType>> searchParameters,
Map<String, String> headers);
/**
* Searches this repository
*
* @see <a href="https://www.hl7.org/fhir/http.html#search">FHIR search</a>
*
* @param <B> a Bundle type
* @param <T> a Resource type
* @param bundleType the class of the Bundle type to return
* @param resourceType the class of the Resource type to search
* @param searchParameters the searchParameters for this search
* @param headers headers for this request, typically key-value pairs of HTTP headers
* @return a Bundle with the results of the search
*/
default <B extends IBaseBundle, T extends IBaseResource> B search(
Class<B> bundleType,
Class<T> resourceType,
Map<String, List<IQueryParameterType>> searchParameters,
Map<String, String> headers) {
ArrayListMultimap<String, List<IQueryParameterType>> multimap = ArrayListMultimap.create();
searchParameters.forEach(multimap::put);
return this.search(bundleType, resourceType, multimap, headers);
}
// Paging starts here
/**

View File

@ -61,6 +61,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
@ -83,10 +84,23 @@ public class FhirTerser {
private static final Pattern COMPARTMENT_MATCHER_PATH =
Pattern.compile("([a-zA-Z.]+)\\.where\\(resolve\\(\\) is ([a-zA-Z]+)\\)");
private static final String USER_DATA_KEY_CONTAIN_RESOURCES_COMPLETED =
FhirTerser.class.getName() + "_CONTAIN_RESOURCES_COMPLETED";
private final FhirContext myContext;
/**
* This comparator sorts IBaseReferences, and places any that are missing an ID at the end. Those with an ID go to the front.
*/
private static final Comparator<IBaseReference> REFERENCES_WITH_IDS_FIRST =
Comparator.nullsLast(Comparator.comparing(ref -> {
if (ref.getResource() == null) return true;
if (ref.getResource().getIdElement() == null) return true;
if (ref.getResource().getIdElement().getValue() == null) return true;
return false;
}));
public FhirTerser(FhirContext theContext) {
super();
myContext = theContext;
@ -1418,6 +1432,13 @@ public class FhirTerser {
private void containResourcesForEncoding(
ContainedResources theContained, IBaseResource theResource, boolean theModifyResource) {
List<IBaseReference> allReferences = getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
// Note that we process all contained resources that have arrived here with an ID contained resources first, so
// that we don't accidentally auto-assign an ID
// which may collide with a resource we have yet to process.
// See: https://github.com/hapifhir/hapi-fhir/issues/6403
allReferences.sort(REFERENCES_WITH_IDS_FIRST);
for (IBaseReference next : allReferences) {
IBaseResource resource = next.getResource();
if (resource == null && next.getReferenceElement().isLocal()) {
@ -1437,11 +1458,11 @@ public class FhirTerser {
IBaseResource resource = next.getResource();
if (resource != null) {
if (resource.getIdElement().isEmpty() || resource.getIdElement().isLocal()) {
if (theContained.getResourceId(resource) != null) {
// Prevent infinite recursion if there are circular loops in the contained resources
IIdType id = theContained.addContained(resource);
if (id == null) {
continue;
}
IIdType id = theContained.addContained(resource);
if (theModifyResource) {
getContainedResourceList(theResource).add(resource);
next.setReference(id.getValue());
@ -1769,7 +1790,6 @@ public class FhirTerser {
public static class ContainedResources {
private long myNextContainedId = 1;
private List<IBaseResource> myResourceList;
private IdentityHashMap<IBaseResource, IIdType> myResourceToIdMap;
private Map<String, IBaseResource> myExistingIdToContainedResourceMap;
@ -1782,6 +1802,11 @@ public class FhirTerser {
}
public IIdType addContained(IBaseResource theResource) {
if (this.getResourceId(theResource) != null) {
// Prevent infinite recursion if there are circular loops in the contained resources
return null;
}
IIdType existing = getResourceToIdMap().get(theResource);
if (existing != null) {
return existing;
@ -1796,7 +1821,10 @@ public class FhirTerser {
if (substring(idPart, 0, 1).equals("#")) {
idPart = idPart.substring(1);
if (StringUtils.isNumeric(idPart)) {
myNextContainedId = Long.parseLong(idPart) + 1;
// If there is a user-supplied numeric contained ID, our auto-assigned IDs should exceed the
// largest
// client-assigned contained ID.
myNextContainedId = Math.max(myNextContainedId, Long.parseLong(idPart) + 1);
}
}
}
@ -1864,6 +1892,7 @@ public class FhirTerser {
}
public void assignIdsToContainedResources() {
// TODO JA: Dead code?
if (!getContainedResources().isEmpty()) {

View File

@ -0,0 +1,43 @@
package ca.uhn.fhir.util.adapters;
import jakarta.annotation.Nonnull;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
public class AdapterManager implements IAdapterManager {
public static final AdapterManager INSTANCE = new AdapterManager();
Set<IAdapterFactory> myAdapterFactories = new HashSet<>();
/**
* Hidden to force shared use of the public INSTANCE.
*/
AdapterManager() {}
public <T> @Nonnull Optional<T> getAdapter(Object theObject, Class<T> theTargetType) {
// todo this can be sped up with a cache of type->Factory.
return myAdapterFactories.stream()
.filter(nextFactory -> nextFactory.getAdapters().stream().anyMatch(theTargetType::isAssignableFrom))
.flatMap(nextFactory -> {
var adapter = nextFactory.getAdapter(theObject, theTargetType);
// can't use Optional.stream() because of our Android target is API level 26/JDK 8.
if (adapter.isPresent()) {
return Stream.of(adapter.get());
} else {
return Stream.empty();
}
})
.findFirst();
}
public void registerFactory(@Nonnull IAdapterFactory theFactory) {
myAdapterFactories.add(theFactory);
}
public void unregisterFactory(@Nonnull IAdapterFactory theFactory) {
myAdapterFactories.remove(theFactory);
}
}

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.util.adapters;
import java.util.Optional;
public class AdapterUtils {
/**
* Main entry point for adapter calls.
* Implements three conversions: cast to the target type, use IAdaptable if present, or lastly try the AdapterManager.INSTANCE.
* @param theObject the object to be adapted
* @param theTargetType the type of the adapter requested
*/
static <T> Optional<T> adapt(Object theObject, Class<T> theTargetType) {
if (theTargetType.isInstance(theObject)) {
//noinspection unchecked
return Optional.of((T) theObject);
}
if (theObject instanceof IAdaptable) {
IAdaptable adaptable = (IAdaptable) theObject;
var adapted = adaptable.getAdapter(theTargetType);
if (adapted.isPresent()) {
return adapted;
}
}
return AdapterManager.INSTANCE.getAdapter(theObject, theTargetType);
}
}

View File

@ -0,0 +1,19 @@
package ca.uhn.fhir.util.adapters;
import jakarta.annotation.Nonnull;
import java.util.Optional;
/**
* Generic version of Eclipse IAdaptable interface.
*/
public interface IAdaptable {
/**
* Get an adapter of requested type.
* @param theTargetType the desired type of the adapter
* @return an adapter of theTargetType if possible, or empty.
*/
default <T> @Nonnull Optional<T> getAdapter(@Nonnull Class<T> theTargetType) {
return AdapterUtils.adapt(this, theTargetType);
}
}

View File

@ -0,0 +1,25 @@
package ca.uhn.fhir.util.adapters;
import java.util.Collection;
import java.util.Optional;
/**
* Interface for external service that builds adaptors for targets.
*/
public interface IAdapterFactory {
/**
* Build an adaptor for the target.
* May return empty() even if the target type is listed in getAdapters() when
* the factory fails to convert a particular instance.
*
* @param theObject the object to be adapted.
* @param theAdapterType the target type
* @return the adapter, if possible.
*/
<T> Optional<T> getAdapter(Object theObject, Class<T> theAdapterType);
/**
* @return the collection of adapter target types handled by this factory.
*/
Collection<Class<?>> getAdapters();
}

View File

@ -0,0 +1,10 @@
package ca.uhn.fhir.util.adapters;
import java.util.Optional;
/**
* Get an adaptor
*/
public interface IAdapterManager {
<T> Optional<T> getAdapter(Object theTarget, Class<T> theAdapter);
}

View File

@ -0,0 +1,20 @@
/**
* Implements the Adapter pattern to allow external classes to extend/adapt existing classes.
* Useful for extending interfaces that are closed to modification, or restricted for classpath reasons.
* <p>
* For clients, the main entry point is {@link ca.uhn.fhir.util.adapters.AdapterUtils#adapt(java.lang.Object, java.lang.Class)}
* which will attempt to cast to the target type, or build an adapter of the target type.
* </p>
* <p>
* For implementors, you can support adaptation via two mechanisms:
* <ul>
* <li>by implementing {@link ca.uhn.fhir.util.adapters.IAdaptable} directly on a class to provide supported adapters,
* <li>or when the class is closed to direct modification, you can implement
* an instance of {@link ca.uhn.fhir.util.adapters.IAdapterFactory} and register
* it with the public {@link ca.uhn.fhir.util.adapters.AdapterManager#INSTANCE}.</li>
* </ul>
* The AdapterUtils.adapt() supports both of these.
* </p>
* Inspired by the Eclipse runtime.
*/
package ca.uhn.fhir.util.adapters;

View File

@ -0,0 +1,78 @@
package ca.uhn.fhir.util.adapters;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
public class AdapterManagerTest {
AdapterManager myAdapterManager = new AdapterManager();
@AfterAll
static void tearDown() {
assertThat(AdapterManager.INSTANCE.myAdapterFactories)
.withFailMessage("Don't dirty the public instance").isEmpty();
}
@Test
void testRegisterFactory_providesAdapter() {
// given
myAdapterManager.registerFactory(new StringToIntFactory());
// when
var result = myAdapterManager.getAdapter("22", Integer.class);
// then
assertThat(result).contains(22);
}
@Test
void testRegisterFactory_wrongTypeStillEmpty() {
// given
myAdapterManager.registerFactory(new StringToIntFactory());
// when
var result = myAdapterManager.getAdapter("22", Float.class);
// then
assertThat(result).isEmpty();
}
@Test
void testUnregisterFactory_providesEmpty() {
// given active factory, now gone.
StringToIntFactory factory = new StringToIntFactory();
myAdapterManager.registerFactory(factory);
myAdapterManager.getAdapter("22", Integer.class);
myAdapterManager.unregisterFactory(factory);
// when
var result = myAdapterManager.getAdapter("22", Integer.class);
// then
assertThat(result).isEmpty();
}
static class StringToIntFactory implements IAdapterFactory {
@Override
public <T> Optional<T> getAdapter(Object theObject, Class<T> theAdapterType) {
if (theObject instanceof String s) {
if (theAdapterType.isAssignableFrom(Integer.class)) {
@SuppressWarnings("unchecked")
T i = (T) Integer.valueOf(s);
return Optional.of(i);
}
}
return Optional.empty();
}
public Collection<Class<?>> getAdapters() {
return List.of(Integer.class);
}
}
}

View File

@ -0,0 +1,123 @@
package ca.uhn.fhir.util.adapters;
import jakarta.annotation.Nonnull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class AdapterUtilsTest {
final private IAdapterFactory myTestFactory = new TestAdaptorFactory();
@AfterEach
void tearDown() {
AdapterManager.INSTANCE.unregisterFactory(myTestFactory);
}
@Test
void testNullDoesNotAdapt() {
// when
var adapted = AdapterUtils.adapt(null, InterfaceA.class);
// then
assertThat(adapted).isEmpty();
}
@Test
void testAdaptObjectImplementingInterface() {
// given
var object = new ClassB();
// when
var adapted = AdapterUtils.adapt(object, InterfaceA.class);
// then
assertThat(adapted)
.isPresent()
.get().isInstanceOf(InterfaceA.class);
assertThat(adapted.get()).withFailMessage("Use object since it implements interface").isSameAs(object);
}
@Test
void testAdaptObjectImplementingAdaptorSupportingInterface() {
// given
var object = new SelfAdaptableClass();
// when
var adapted = AdapterUtils.adapt(object, InterfaceA.class);
// then
assertThat(adapted)
.isPresent()
.get().isInstanceOf(InterfaceA.class);
}
@Test
void testAdaptObjectViaAdapterManager() {
// given
var object = new ManagerAdaptableClass();
AdapterManager.INSTANCE.registerFactory(myTestFactory);
// when
var adapted = AdapterUtils.adapt(object, InterfaceA.class);
// then
assertThat(adapted)
.isPresent()
.get().isInstanceOf(InterfaceA.class);
}
interface InterfaceA {
}
static class ClassB implements InterfaceA {
}
/** class that can adapt itself to IAdaptable */
static class SelfAdaptableClass implements IAdaptable {
@Nonnull
@Override
public <T> Optional<T> getAdapter(@Nonnull Class<T> theTargetType) {
if (theTargetType.isAssignableFrom(InterfaceA.class)) {
T value = theTargetType.cast(buildInterfaceAWrapper(this));
return Optional.of(value);
}
return Optional.empty();
}
}
private static @Nonnull InterfaceA buildInterfaceAWrapper(Object theObject) {
return new InterfaceA() {};
}
/** Class that relies on an external IAdapterFactory */
static class ManagerAdaptableClass {
}
static class TestAdaptorFactory implements IAdapterFactory {
@Override
public <T> Optional<T> getAdapter(Object theObject, Class<T> theAdapterType) {
if (theObject instanceof ManagerAdaptableClass && theAdapterType == InterfaceA.class) {
T adapter = theAdapterType.cast(buildInterfaceAWrapper(theObject));
return Optional.of(adapter);
}
return Optional.empty();
}
@Override
public Collection<Class<?>> getAdapters() {
return Set.of(InterfaceA.class);
}
}
}

View File

@ -0,0 +1,6 @@
---
type: fix
jira: SMILE-8969
title: "Fixed a rare bug in the JSON Parser, wherein client-assigned contained resource IDs could collide with server-assigned contained IDs. For example if a
resource had a client-assigned contained ID of `#2`, and a contained resource with no ID, then depending on the processing order, the parser could occasionally
provide duplicate contained resource IDs, leading to non-deterministic behaviour."

View File

@ -0,0 +1,8 @@
---
type: fix
issue: 6404
title: "Searches using fulltext search that combined `_lastUpdated` query parameter with
any other (supported) fulltext query parameter would find no matches, even
if matches existed.
This has been corrected.
"

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 6419
title: "Previously, on Postgres, the `$reindex` operation with `optimizeStorage` set to `ALL_VERSIONS` would process
only a subset of versions if there were more than 100 versions to be processed for a resource. This has been fixed
so that all versions of the resource are now processed."

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 6420
title: "Previously, when the `$reindex` operation is run for a single FHIR resource with `optimizeStorage` set to
`ALL_VERSIONS`, none of the versions of the resource were processed in `hfj_res_ver` table. This has been fixed."

View File

@ -0,0 +1,7 @@
---
type: fix
issue: 6422
title: "Previously, since 7.4.4 the validation issue detail codes were not translated correctly for Remote Terminology
validateCode calls. The detail code used was `invalid-code` for all use-cases which resulted in profile binding strength
not being applied to the issue severity as expected when validating resources against a profile.
This has been fixed and issue detail codes are translated correctly."

View File

@ -0,0 +1,8 @@
---
type: fix
issue: 6440
title: "Previously, if an `IInterceptorBroadcaster` was set in a `RequestDetails` object,
`STORAGE_PRECHECK_FOR_CACHED_SEARCH` hooks that were registered to that `IInterceptorBroadcaster` were not
called. Also, if an `IInterceptorBroadcaster` was set in the `RequestDetails` object, the boolean return value of the hooks
registered to that `IInterceptorBroadcaster` were not taken into account. This second issue existed for all pointcuts
that returned a boolean type, not just for `STORAGE_PRECHECK_FOR_CACHED_SEARCH`. These issues have now been fixed."

View File

@ -0,0 +1,4 @@
---
type: add
issue: 6445
title: "Add Multimap versions of the search() methods to Repository to support queries like `Patient?_tag=a&_tag=b`"

View File

@ -23,3 +23,8 @@ If `true`, this will return summarized subject bundle with only detectedIssue.
The `subject` parameter of the `Questionnaire/$populate` operation has been changed to expect a `Reference` as specified
in the SDC IG.
# Fulltext Search with _lastUpdated Filter
Fulltext searches have been updated to support `_lastUpdated` search parameter. A reindexing of Search Parameters
is required to migrate old data to support the `_lastUpdated` search parameter.

View File

@ -48,24 +48,24 @@ Additional parameters have been added to support CQL evaluation.
The following parameters are supported for the `Questionnaire/$populate` operation:
| Parameter | Type | Description |
|---------------------|---------------|-------------|
| questionnaire | Questionnaire | The Questionnaire to populate. Used when the operation is invoked at the 'type' level. |
| canonical | canonical | The canonical identifier for the Questionnaire (optionally version-specific). |
| url | uri | Canonical URL of the Questionnaire when invoked at the resource type level. This is exclusive with the questionnaire and canonical parameters. |
| version | string | Version of the Questionnaire when invoked at the resource type level. This is exclusive with the questionnaire and canonical parameters. |
| subject | Reference | The resource that is to be the QuestionnaireResponse.subject. The QuestionnaireResponse instance will reference the provided subject. |
| context | | Resources containing information to be used to help populate the QuestionnaireResponse. |
| context.name | string | The name of the launchContext or root Questionnaire variable the passed content should be used as for population purposes. The name SHALL correspond to a launchContext or variable delared at the root of the Questionnaire. |
| context.reference | Reference | The actual resource (or resources) to use as the value of the launchContext or variable. |
| local | boolean | Whether the server should use what resources and other knowledge it has about the referenced subject when pre-populating answers to questions. |
| launchContext | Extension | The [Questionnaire Launch Context](https://hl7.org/fhir/uv/sdc/StructureDefinition-sdc-questionnaire-launchContext.html) extension containing Resources that provide context for form processing logic (pre-population) when creating/displaying/editing a QuestionnaireResponse. |
| parameters | Parameters | Any input parameters defined in libraries referenced by the Questionnaire. |
| useServerData | boolean | Whether to use data from the server performing the evaluation. |
| data | Bundle | Data to be made available during CQL evaluation. |
| dataEndpoint | Endpoint | An endpoint to use to access data referenced by retrieve operations in libraries referenced by the Questionnaire. |
| contentEndpoint | Endpoint | An endpoint to use to access content (i.e. libraries) referenced by the Questionnaire. |
| terminologyEndpoint | Endpoint | An endpoint to use to access terminology (i.e. valuesets, codesystems, and membership testing) referenced by the Questionnaire. |
| Parameter | Type | Description |
|---------------------|--------------------|-------------|
| questionnaire | Questionnaire | The Questionnaire to populate. Used when the operation is invoked at the 'type' level. |
| canonical | canonical | The canonical identifier for the Questionnaire (optionally version-specific). |
| url | uri | Canonical URL of the Questionnaire when invoked at the resource type level. This is exclusive with the questionnaire and canonical parameters. |
| version | string | Version of the Questionnaire when invoked at the resource type level. This is exclusive with the questionnaire and canonical parameters. |
| subject | Reference | The resource that is to be the QuestionnaireResponse.subject. The QuestionnaireResponse instance will reference the provided subject. |
| context | | Resources containing information to be used to help populate the QuestionnaireResponse. |
| context.name | string | The name of the launchContext or root Questionnaire variable the passed content should be used as for population purposes. The name SHALL correspond to a launchContext or variable declared at the root of the Questionnaire. |
| context.content | Reference/Resource | The actual resource (or reference) to use as the value of the launchContext or variable. |
| local | boolean | Whether the server should use what resources and other knowledge it has about the referenced subject when pre-populating answers to questions. |
| launchContext | Extension | The [Questionnaire Launch Context](https://hl7.org/fhir/uv/sdc/StructureDefinition-sdc-questionnaire-launchContext.html) extension containing Resources that provide context for form processing logic (pre-population) when creating/displaying/editing a QuestionnaireResponse. |
| parameters | Parameters | Any input parameters defined in libraries referenced by the Questionnaire. |
| useServerData | boolean | Whether to use data from the server performing the evaluation. |
| data | Bundle | Data to be made available during CQL evaluation. |
| dataEndpoint | Endpoint | An endpoint to use to access data referenced by retrieve operations in libraries referenced by the Questionnaire. |
| contentEndpoint | Endpoint | An endpoint to use to access content (i.e. libraries) referenced by the Questionnaire. |
| terminologyEndpoint | Endpoint | An endpoint to use to access terminology (i.e. valuesets, codesystems, and membership testing) referenced by the Questionnaire. |
## Extract

View File

@ -981,12 +981,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
entity.setIndexStatus(INDEX_STATUS_INDEXED);
}
if (myFulltextSearchSvc != null && !myFulltextSearchSvc.isDisabled()) {
if (myFulltextSearchSvc != null && !myFulltextSearchSvc.isDisabled() && changed.isChanged()) {
populateFullTextFields(myContext, theResource, entity, newParams);
}
} else {
entity.setUpdated(theTransactionDetails.getTransactionDate());
entity.setIndexStatus(null);
@ -1018,16 +1016,19 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
* Save the resource itself
*/
if (entity.getId() == null) {
// create
myEntityManager.persist(entity);
postPersist(entity, (T) theResource, theRequest);
} else if (entity.getDeleted() != null) {
// delete
entity = myEntityManager.merge(entity);
postDelete(entity);
} else {
// update
entity = myEntityManager.merge(entity);
postUpdate(entity, (T) theResource, theRequest);
@ -1679,7 +1680,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
theEntity.setContentText(parseContentTextIntoWords(theContext, theResource));
if (myStorageSettings.isAdvancedHSearchIndexing()) {
ExtendedHSearchIndexData hSearchIndexData =
myFulltextSearchSvc.extractLuceneIndexData(theResource, theNewParams);
myFulltextSearchSvc.extractLuceneIndexData(theResource, theEntity, theNewParams);
theEntity.setLuceneIndexData(hSearchIndexData);
}
}

View File

@ -133,6 +133,7 @@ import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@ -1693,9 +1694,15 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
if (theOptimizeStorageMode == ReindexParameters.OptimizeStorageModeEnum.ALL_VERSIONS) {
int pageSize = 100;
for (int page = 0; ((long) page * pageSize) < entity.getVersion(); page++) {
// We need to sort the pages, because we are updating the same data we are paging through.
// If not sorted explicitly, a database like Postgres returns the same data multiple times on
// different pages as the underlying data gets updated.
PageRequest pageRequest = PageRequest.of(page, pageSize, Sort.by("myId"));
Slice<ResourceHistoryTable> historyEntities =
myResourceHistoryTableDao.findForResourceIdAndReturnEntitiesAndFetchProvenance(
PageRequest.of(page, pageSize), entity.getId(), historyEntity.getVersion());
pageRequest, entity.getId(), historyEntity.getVersion());
for (ResourceHistoryTable next : historyEntities) {
reindexOptimizeStorageHistoryEntity(entity, next);
}

View File

@ -205,7 +205,7 @@ public abstract class BaseHapiFhirSystemDao<T extends IBaseBundle, MT> extends B
*
* However, for realistic average workloads, this should reduce the number of round trips.
*/
if (idChunk.size() >= 2) {
if (!idChunk.isEmpty()) {
List<ResourceTable> entityChunk = prefetchResourceTableHistoryAndProvenance(idChunk);
if (thePreFetchIndexes) {

View File

@ -135,13 +135,13 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
@Override
public ExtendedHSearchIndexData extractLuceneIndexData(
IBaseResource theResource, ResourceIndexedSearchParams theNewParams) {
IBaseResource theResource, ResourceTable theEntity, ResourceIndexedSearchParams theNewParams) {
String resourceType = myFhirContext.getResourceType(theResource);
ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams(
resourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
ExtendedHSearchIndexExtractor extractor = new ExtendedHSearchIndexExtractor(
myStorageSettings, myFhirContext, activeSearchParams, mySearchParamExtractor);
return extractor.extract(theResource, theNewParams);
return extractor.extract(theResource, theEntity, theNewParams);
}
@Override

View File

@ -89,7 +89,7 @@ public interface IFulltextSearchSvc {
boolean isDisabled();
ExtendedHSearchIndexData extractLuceneIndexData(
IBaseResource theResource, ResourceIndexedSearchParams theNewParams);
IBaseResource theResource, ResourceTable theEntity, ResourceIndexedSearchParams theNewParams);
/**
* Returns true if the parameter map can be handled for hibernate search.

View File

@ -392,7 +392,7 @@ public class ExtendedHSearchClauseBuilder {
/**
* Create date clause from date params. The date lower and upper bounds are taken
* into considertion when generating date query ranges
* into consideration when generating date query ranges
*
* <p>Example 1 ('eq' prefix/empty): <code>http://fhirserver/Observation?date=eq2020</code>
* would generate the following search clause

View File

@ -25,6 +25,8 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.TagDefinition;
import ca.uhn.fhir.jpa.model.search.CompositeSearchIndexData;
import ca.uhn.fhir.jpa.model.search.DateSearchIndexData;
import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData;
@ -37,6 +39,7 @@ import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import ca.uhn.fhir.util.MetaUtil;
import com.google.common.base.Strings;
import jakarta.annotation.Nonnull;
import org.apache.commons.lang3.ObjectUtils;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -73,9 +76,10 @@ public class ExtendedHSearchIndexExtractor {
mySearchParamExtractor = theSearchParamExtractor;
}
@Nonnull
public ExtendedHSearchIndexData extract(IBaseResource theResource, ResourceIndexedSearchParams theNewParams) {
ExtendedHSearchIndexData retVal = new ExtendedHSearchIndexData(myContext, myJpaStorageSettings, theResource);
public ExtendedHSearchIndexData extract(
IBaseResource theResource, ResourceTable theEntity, ResourceIndexedSearchParams theNewParams) {
ExtendedHSearchIndexData retVal =
new ExtendedHSearchIndexData(myContext, myJpaStorageSettings, theResource, theEntity);
if (myJpaStorageSettings.isStoreResourceInHSearchIndex()) {
retVal.setRawResourceData(myContext.newJsonParser().encodeResourceToString(theResource));
@ -113,11 +117,27 @@ public class ExtendedHSearchIndexExtractor {
.filter(nextParam -> !nextParam.isMissing())
.forEach(nextParam -> retVal.addUriIndexData(nextParam.getParamName(), nextParam.getUri()));
theResource.getMeta().getTag().forEach(tag -> retVal.addTokenIndexData("_tag", tag));
theEntity.getTags().forEach(tag -> {
TagDefinition td = tag.getTag();
theResource.getMeta().getSecurity().forEach(sec -> retVal.addTokenIndexData("_security", sec));
theResource.getMeta().getProfile().forEach(prof -> retVal.addUriIndexData("_profile", prof.getValue()));
IBaseCoding coding = (IBaseCoding) myContext.getVersion().newCodingDt();
coding.setVersion(td.getVersion());
coding.setDisplay(td.getDisplay());
coding.setCode(td.getCode());
coding.setSystem(td.getSystem());
coding.setUserSelected(ObjectUtils.defaultIfNull(td.getUserSelected(), false));
switch (td.getTagType()) {
case TAG:
retVal.addTokenIndexData("_tag", coding);
break;
case PROFILE:
retVal.addUriIndexData("_profile", coding.getCode());
break;
case SECURITY_LABEL:
retVal.addTokenIndexData("_security", coding);
break;
}
});
String source = MetaUtil.getSource(myContext, theResource.getMeta());
if (isNotBlank(source)) {
@ -127,20 +147,14 @@ public class ExtendedHSearchIndexExtractor {
theNewParams.myCompositeParams.forEach(nextParam ->
retVal.addCompositeIndexData(nextParam.getSearchParamName(), buildCompositeIndexData(nextParam)));
if (theResource.getMeta().getLastUpdated() != null) {
int ordinal = ResourceIndexedSearchParamDate.calculateOrdinalValue(
theResource.getMeta().getLastUpdated())
if (theEntity.getUpdated() != null && !theEntity.getUpdated().isEmpty()) {
int ordinal = ResourceIndexedSearchParamDate.calculateOrdinalValue(theEntity.getUpdatedDate())
.intValue();
retVal.addDateIndexData(
"_lastUpdated",
theResource.getMeta().getLastUpdated(),
ordinal,
theResource.getMeta().getLastUpdated(),
ordinal);
"_lastUpdated", theEntity.getUpdatedDate(), ordinal, theEntity.getUpdatedDate(), ordinal);
}
if (!theNewParams.myLinks.isEmpty()) {
// awkwardly, links are indexed by jsonpath, not by search param.
// so we re-build the linkage.
Map<String, List<String>> linkPathToParamName = new HashMap<>();

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
import ca.uhn.fhir.rest.param.CompositeParam;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
@ -89,19 +90,29 @@ public class ExtendedHSearchSearchBuilder {
* be inaccurate and wrong.
*/
public boolean canUseHibernateSearch(
String theResourceType, SearchParameterMap myParams, ISearchParamRegistry theSearchParamRegistry) {
String theResourceType, SearchParameterMap theParams, ISearchParamRegistry theSearchParamRegistry) {
boolean canUseHibernate = false;
ResourceSearchParams resourceActiveSearchParams = theSearchParamRegistry.getActiveSearchParams(
theResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
for (String paramName : myParams.keySet()) {
theResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
// special SearchParam handling:
// _lastUpdated
if (theParams.getLastUpdated() != null) {
canUseHibernate = !illegalForHibernateSearch(Constants.PARAM_LASTUPDATED, resourceActiveSearchParams);
if (!canUseHibernate) {
return false;
}
}
for (String paramName : theParams.keySet()) {
// is this parameter supported?
if (illegalForHibernateSearch(paramName, resourceActiveSearchParams)) {
canUseHibernate = false;
} else {
// are the parameter values supported?
canUseHibernate =
myParams.get(paramName).stream()
theParams.get(paramName).stream()
.flatMap(Collection::stream)
.collect(Collectors.toList())
.stream()
@ -136,6 +147,7 @@ public class ExtendedHSearchSearchBuilder {
// not yet supported in HSearch
myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE
&& supportsLastUpdated(myParams)
&& // ???
myParams.entrySet().stream()
.filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey()))
@ -145,6 +157,19 @@ public class ExtendedHSearchSearchBuilder {
.allMatch(this::isParamTypeSupported);
}
private boolean supportsLastUpdated(SearchParameterMap theMap) {
if (theMap.getLastUpdated() == null || theMap.getLastUpdated().isEmpty()) {
return true;
}
DateRangeParam lastUpdated = theMap.getLastUpdated();
return lastUpdated.getLowerBound() != null
&& isParamTypeSupported(lastUpdated.getLowerBound())
&& lastUpdated.getUpperBound() != null
&& isParamTypeSupported(lastUpdated.getUpperBound());
}
/**
* Do we support this query param type+modifier?
* <p>

View File

@ -605,12 +605,12 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
.add(SearchParameterMap.class, theParams)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
Object outcome = CompositeInterceptorBroadcaster.doCallHooksAndReturnObject(
boolean canUseCache = CompositeInterceptorBroadcaster.doCallHooks(
myInterceptorBroadcaster,
theRequestDetails,
Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH,
params);
if (Boolean.FALSE.equals(outcome)) {
if (!canUseCache) {
return null;
}

View File

@ -1044,7 +1044,8 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
if (theExpansionOptions != null
&& !theExpansionOptions.isFailOnMissingCodeSystem()
// Code system is unknown, therefore NOT_FOUND
&& e.getCodeValidationIssue().getCoding() == CodeValidationIssueCoding.NOT_FOUND) {
&& e.getCodeValidationIssue()
.hasIssueDetailCode(CodeValidationIssueCoding.NOT_FOUND.getCode())) {
return;
}
throw new InternalErrorException(Msg.code(888) + e);
@ -2190,7 +2191,7 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
.setSeverity(IssueSeverity.ERROR)
.setCodeSystemVersion(theCodeSystemVersion)
.setMessage(theMessage)
.addCodeValidationIssue(new CodeValidationIssue(
.addIssue(new CodeValidationIssue(
theMessage,
IssueSeverity.ERROR,
CodeValidationIssueCode.CODE_INVALID,

View File

@ -121,7 +121,6 @@ import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
@ -130,13 +129,11 @@ import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.model.util.UcumServiceUtil.UCUM_CODESYSTEM_URL;
import static ca.uhn.fhir.rest.api.Constants.CHARSET_UTF8;
import static ca.uhn.fhir.rest.api.Constants.HEADER_CACHE_CONTROL;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
@ExtendWith(MockitoExtension.class)
@ -294,6 +291,20 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
}
}
@Test //TODO LS : This test fails, and did not before.
public void testNoOpUpdateDoesNotModifyLastUpdated() throws InterruptedException {
myStorageSettings.setAdvancedHSearchIndexing(true);
Patient patient = new Patient();
patient.getNameFirstRep().setFamily("graham").addGiven("gary");
patient = (Patient) myPatientDao.create(patient).getResource();
Date originalLastUpdated = patient.getMeta().getLastUpdated();
patient = (Patient) myPatientDao.update(patient).getResource();
Date newLastUpdated = patient.getMeta().getLastUpdated();
assertThat(originalLastUpdated).isEqualTo(newLastUpdated);
}
@Test
public void testFullTextSearchesArePerformanceLogged() {

View File

@ -108,7 +108,6 @@ public class ValueSetExpansionR4ElasticsearchIT extends BaseJpaTest implements I
when(mySrd.getUserData().getOrDefault(MAKE_LOADING_VERSION_CURRENT, Boolean.TRUE)).thenReturn(Boolean.TRUE);
}
@AfterEach
public void after() {
myStorageSettings.setMaximumExpansionSize(JpaStorageSettings.DEFAULT_MAX_EXPANSION_SIZE);
@ -217,7 +216,6 @@ public class ValueSetExpansionR4ElasticsearchIT extends BaseJpaTest implements I
}
myTermCodeSystemStorageSvc.applyDeltaCodeSystemsAdd(CS_URL, additions);
// Codes available exceeds the max
myStorageSettings.setMaximumExpansionSize(50);
ValueSet vs = new ValueSet();
@ -236,7 +234,6 @@ public class ValueSetExpansionR4ElasticsearchIT extends BaseJpaTest implements I
include.setSystem(CS_URL);
ValueSet outcome = myTermSvc.expandValueSet(null, vs);
assertThat(outcome.getExpansion().getContains()).hasSize(109);
}
@Test

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.mdm.helper;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
@ -23,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import java.util.function.Supplier;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
/**
@ -78,6 +80,7 @@ public abstract class BaseMdmHelper implements BeforeEachCallback, AfterEachCall
//they are coming from an external HTTP Request.
MockitoAnnotations.initMocks(this);
when(myMockSrd.getInterceptorBroadcaster()).thenReturn(myMockInterceptorBroadcaster);
when(myMockInterceptorBroadcaster.callHooks(any(Pointcut.class), any(HookParams.class))).thenReturn(true);
when(myMockSrd.getServletRequest()).thenReturn(myMockServletRequest);
when(myMockSrd.getServer()).thenReturn(myMockRestfulServer);
when(myMockSrd.getRequestId()).thenReturn("MOCK_REQUEST");

View File

@ -20,6 +20,7 @@
package ca.uhn.fhir.jpa.model.search;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import com.google.common.collect.HashMultimap;
@ -57,12 +58,17 @@ public class ExtendedHSearchIndexData {
private String myForcedId;
private String myResourceJSON;
private IBaseResource myResource;
private ResourceTable myEntity;
public ExtendedHSearchIndexData(
FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) {
FhirContext theFhirContext,
StorageSettings theStorageSettings,
IBaseResource theResource,
ResourceTable theEntity) {
this.myFhirContext = theFhirContext;
this.myStorageSettings = theStorageSettings;
myResource = theResource;
myEntity = theEntity;
}
private <V> BiConsumer<String, V> ifNotContained(BiConsumer<String, V> theIndexWriter) {

View File

@ -62,6 +62,9 @@ public abstract class BaseComboParamsR4Test extends BaseJpaR4Test {
myMessages.add("REUSING CACHED SEARCH");
return null;
});
// allow searches to use cached results
when(myInterceptorBroadcaster.callHooks(eq(Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH), ArgumentMatchers.any(HookParams.class))).thenReturn(true);
}
@AfterEach

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
@ -23,6 +24,8 @@ import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.ServiceRequest;
import org.hl7.fhir.r4.model.ServiceRequest.ServiceRequestIntent;
import org.hl7.fhir.r4.model.ServiceRequest.ServiceRequestStatus;
import org.hl7.fhir.r4.model.Specimen;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@ -2549,7 +2549,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output));
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
myCaptureQueriesListener.logInsertQueriesForCurrentThread();
assertEquals(2, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
myCaptureQueriesListener.logUpdateQueriesForCurrentThread();

View File

@ -428,7 +428,7 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
IdType observationId = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
// Make sure we're not introducing any extra DB operations
assertThat(myCaptureQueriesListener.logSelectQueries()).hasSize(3);
assertThat(myCaptureQueriesListener.logSelectQueries()).hasSize(2);
// Read back and verify that reference is now versioned
observation = myObservationDao.read(observationId);
@ -463,7 +463,7 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test {
IdType observationId = new IdType(outcome.getEntry().get(1).getResponse().getLocation());
// Make sure we're not introducing any extra DB operations
assertThat(myCaptureQueriesListener.logSelectQueries()).hasSize(4);
assertThat(myCaptureQueriesListener.logSelectQueries()).hasSize(3);
// Read back and verify that reference is now versioned
observation = myObservationDao.read(observationId);

View File

@ -14,9 +14,6 @@ import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.param.StringAndListParam;
import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
@ -29,7 +26,6 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@ -304,169 +300,4 @@ public class FhirSearchDaoR4Test extends BaseJpaR4Test implements IR4SearchIndex
assertThat(JpaPid.toLongList(found)).isEmpty();
}
}
@Test
public void testSearchNarrativeWithLuceneSearch() {
final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10;
List<String> expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate);
for (int i = 0; i < numberOfPatientsToCreate; i++) {
Patient patient = new Patient();
patient.getText().setDivAsString("<div>AAAS<p>FOO</p> CCC </div>");
expectedActivePatientIds.add(myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless().getIdPart());
}
{
Patient patient = new Patient();
patient.getText().setDivAsString("<div>AAAB<p>FOO</p> CCC </div>");
myPatientDao.create(patient, mySrd);
}
{
Patient patient = new Patient();
patient.getText().setDivAsString("<div>ZZYZXY</div>");
myPatientDao.create(patient, mySrd);
}
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true);
map.add(Constants.PARAM_TEXT, new StringParam("AAAS"));
IBundleProvider searchResultBundle = myPatientDao.search(map, mySrd);
List<String> resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds();
assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds);
}
@Test
public void searchLuceneAndJPA_withLuceneMatchingButJpaNot_returnsNothing() {
// setup
int numToCreate = 2 * SearchBuilder.getMaximumPageSize() + 10;
// create resources
for (int i = 0; i < numToCreate; i++) {
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier()
.setSystem("http://fhir.com")
.setValue("ZYX");
patient.getText().setDivAsString("<div>ABC</div>");
myPatientDao.create(patient, mySrd);
}
// test
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.setSearchTotalMode(SearchTotalModeEnum.ACCURATE);
TokenAndListParam tokenAndListParam = new TokenAndListParam();
tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true")));
map.add("active", tokenAndListParam);
map.add(Constants.PARAM_TEXT, new StringParam("ABC"));
map.add("identifier", new TokenParam(null, "not found"));
IBundleProvider provider = myPatientDao.search(map, mySrd);
// verify
assertEquals(0, provider.getAllResources().size());
}
@Test
public void searchLuceneAndJPA_withLuceneBroadAndJPASearchNarrow_returnsFoundResults() {
// setup
int numToCreate = 2 * SearchBuilder.getMaximumPageSize() + 10;
String identifierToFind = "bcde";
// create patients
for (int i = 0; i < numToCreate; i++) {
Patient patient = new Patient();
patient.setActive(true);
String identifierVal = i == numToCreate - 10 ? identifierToFind:
"abcd";
patient.addIdentifier()
.setSystem("http://fhir.com")
.setValue(identifierVal);
patient.getText().setDivAsString(
"<div>FINDME</div>"
);
myPatientDao.create(patient, mySrd);
}
// test
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.setSearchTotalMode(SearchTotalModeEnum.ACCURATE);
TokenAndListParam tokenAndListParam = new TokenAndListParam();
tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true")));
map.add("active", tokenAndListParam);
map.add(Constants.PARAM_TEXT, new StringParam("FINDME"));
map.add("identifier", new TokenParam(null, identifierToFind));
IBundleProvider provider = myPatientDao.search(map, mySrd);
// verify
List<String> ids = provider.getAllResourceIds();
assertEquals(1, ids.size());
}
@Test
public void testLuceneNarrativeSearchQueryIntersectingJpaQuery() {
final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10;
List<String> expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate);
// create active and non-active patients with the same narrative
for (int i = 0; i < numberOfPatientsToCreate; i++) {
Patient activePatient = new Patient();
activePatient.getText().setDivAsString("<div>AAAS<p>FOO</p> CCC </div>");
activePatient.setActive(true);
String patientId = myPatientDao.create(activePatient, mySrd).getId().toUnqualifiedVersionless().getIdPart();
expectedActivePatientIds.add(patientId);
Patient nonActivePatient = new Patient();
nonActivePatient.getText().setDivAsString("<div>AAAS<p>FOO</p> CCC </div>");
nonActivePatient.setActive(false);
myPatientDao.create(nonActivePatient, mySrd);
}
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true);
TokenAndListParam tokenAndListParam = new TokenAndListParam();
tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true")));
map.add("active", tokenAndListParam);
map.add(Constants.PARAM_TEXT, new StringParam("AAAS"));
IBundleProvider searchResultBundle = myPatientDao.search(map, mySrd);
List<String> resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds();
assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds);
}
@Test
public void testLuceneContentSearchQueryIntersectingJpaQuery() {
final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10;
final String patientFamilyName = "Flanders";
List<String> expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate);
// create active and non-active patients with the same narrative
for (int i = 0; i < numberOfPatientsToCreate; i++) {
Patient activePatient = new Patient();
activePatient.addName().setFamily(patientFamilyName);
activePatient.setActive(true);
String patientId = myPatientDao.create(activePatient, mySrd).getId().toUnqualifiedVersionless().getIdPart();
expectedActivePatientIds.add(patientId);
Patient nonActivePatient = new Patient();
nonActivePatient.addName().setFamily(patientFamilyName);
nonActivePatient.setActive(false);
myPatientDao.create(nonActivePatient, mySrd);
}
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true);
TokenAndListParam tokenAndListParam = new TokenAndListParam();
tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true")));
map.add("active", tokenAndListParam);
map.add(Constants.PARAM_CONTENT, new StringParam(patientFamilyName));
IBundleProvider searchResultBundle = myPatientDao.search(map, mySrd);
List<String> resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds();
assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds);
}
}

View File

@ -263,6 +263,8 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myStorageSettings.setSearchPreFetchThresholds(new JpaStorageSettings().getSearchPreFetchThresholds());
}
@Test
public void testParameterWithNoValueThrowsError_InvalidChainOnCustomSearch() throws IOException {
SearchParameter searchParameter = new SearchParameter();

View File

@ -180,6 +180,59 @@ public class ReindexTaskTest extends BaseJpaR4Test {
}
@Test
public void testOptimizeStorage_AllVersions_SingleResourceWithMultipleVersion() {
// this difference of this test from testOptimizeStorage_AllVersions is that this one has only 1 resource
// (with multiple versions) in the db. There was a bug where if only one resource were being re-indexed, the
// resource wasn't processed for optimize storage.
// Setup
IIdType patientId = createPatient(withActiveTrue());
for (int i = 0; i < 10; i++) {
Patient p = new Patient();
p.setId(patientId.toUnqualifiedVersionless());
p.setActive(true);
p.addIdentifier().setValue(String.valueOf(i));
myPatientDao.update(p, mySrd);
}
// Move resource text to compressed storage, which we don't write to anymore but legacy
// data may exist that was previously stored there, so we're simulating that.
List<ResourceHistoryTable> allHistoryEntities = runInTransaction(() -> myResourceHistoryTableDao.findAll());
allHistoryEntities.forEach(t->relocateResourceTextToCompressedColumn(t.getResourceId(), t.getVersion()));
runInTransaction(()->{
assertEquals(11, myResourceHistoryTableDao.count());
for (ResourceHistoryTable history : myResourceHistoryTableDao.findAll()) {
assertNull(history.getResourceTextVc());
assertNotNull(history.getResource());
}
});
// execute
JobInstanceStartRequest startRequest = new JobInstanceStartRequest();
startRequest.setJobDefinitionId(JOB_REINDEX);
startRequest.setParameters(
new ReindexJobParameters()
.setOptimizeStorage(ReindexParameters.OptimizeStorageModeEnum.ALL_VERSIONS)
.setReindexSearchParameters(ReindexParameters.ReindexSearchParametersEnum.NONE)
);
Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(mySrd, startRequest);
myBatch2JobHelper.awaitJobCompletion(startResponse);
// validate
runInTransaction(()->{
assertEquals(11, myResourceHistoryTableDao.count());
for (ResourceHistoryTable history : myResourceHistoryTableDao.findAll()) {
assertNotNull(history.getResourceTextVc());
assertNull(history.getResource());
}
});
Patient patient = myPatientDao.read(patientId, mySrd);
assertTrue(patient.getActive());
}
@Test
public void testOptimizeStorage_AllVersions_CopyProvenanceEntityData() {
// Setup

View File

@ -1,6 +1,5 @@
package ca.uhn.fhir.jpa.search;
import static org.junit.jupiter.api.Assertions.assertEquals;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.dao.search.ExtendedHSearchResourceProjection;
@ -12,6 +11,7 @@ import org.junit.jupiter.api.Test;
import static ca.uhn.fhir.jpa.dao.search.ExtendedHSearchResourceProjection.RESOURCE_NOT_STORED_ERROR;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class ExtendedHSearchResourceProjectionTest {
@ -57,8 +57,4 @@ class ExtendedHSearchResourceProjectionTest {
() -> new ExtendedHSearchResourceProjection(22, null, ""));
assertEquals(Msg.code(2130) + RESOURCE_NOT_STORED_ERROR + "22", ex.getMessage());
}
}

View File

@ -1,29 +1,21 @@
package ca.uhn.fhir.jpa.provider.r4;
package ca.uhn.fhir.jpa.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.config.JpaConfig;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import jakarta.servlet.http.HttpServletRequest;
import ca.uhn.fhir.test.utilities.validation.IValidationProviders;
import ca.uhn.fhir.test.utilities.validation.IValidationProvidersR4;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.UriType;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.AfterEach;
@ -33,9 +25,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import java.util.ArrayList;
import java.util.List;
import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_VALIDATE_CODE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -43,15 +33,15 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/*
/**
* This set of integration tests that instantiates and injects an instance of
* {@link org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport}
* into the ValidationSupportChain, which tests the logic of dynamically selecting the correct Remote Terminology
* implementation. It also exercises the code found in
* {@link org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport#invokeRemoteValidateCode}
* implementation. It also exercises the validateCode output translation code found in
* {@link org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport}
*/
public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ValidateCodeOperationWithRemoteTerminologyR4Test.class);
public class ValidateCodeWithRemoteTerminologyR4Test extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ValidateCodeWithRemoteTerminologyR4Test.class);
private static final String DISPLAY = "DISPLAY";
private static final String DISPLAY_BODY_MASS_INDEX = "Body mass index (BMI) [Ratio]";
private static final String CODE_BODY_MASS_INDEX = "39156-5";
@ -64,8 +54,8 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour
protected static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx);
private RemoteTerminologyServiceValidationSupport mySvc;
private MyCodeSystemProvider myCodeSystemProvider;
private MyValueSetProvider myValueSetProvider;
private IValidationProviders.MyValidationProvider<CodeSystem> myCodeSystemProvider;
private IValidationProviders.MyValidationProvider<ValueSet> myValueSetProvider;
@Autowired
@Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN)
@ -76,8 +66,8 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour
String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort();
mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl);
myValidationSupportChain.addValidationSupport(0, mySvc);
myCodeSystemProvider = new MyCodeSystemProvider();
myValueSetProvider = new MyValueSetProvider();
myCodeSystemProvider = new IValidationProvidersR4.MyCodeSystemProviderR4();
myValueSetProvider = new IValidationProvidersR4.MyValueSetProviderR4();
ourRestfulServerExtension.registerProvider(myCodeSystemProvider);
ourRestfulServerExtension.registerProvider(myValueSetProvider);
}
@ -103,11 +93,11 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour
@Test
public void validateCodeOperationOnCodeSystem_byCodingAndUrl_usingBuiltInCodeSystems() {
myCodeSystemProvider.myReturnCodeSystems = new ArrayList<>();
myCodeSystemProvider.myReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/v2-0247"));
myCodeSystemProvider.myReturnParams = new Parameters();
myCodeSystemProvider.myReturnParams.addParameter("result", true);
myCodeSystemProvider.myReturnParams.addParameter("display", DISPLAY);
final String code = "P";
final String system = CODE_SYSTEM_V2_0247_URI;;
Parameters params = new Parameters().addParameter("result", true).addParameter("display", DISPLAY);
setupCodeSystemValidateCode(system, code, params);
logAllConcepts();
@ -115,8 +105,8 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour
.operation()
.onType(CodeSystem.class)
.named(JpaConstants.OPERATION_VALIDATE_CODE)
.withParameter(Parameters.class, "coding", new Coding().setSystem(CODE_SYSTEM_V2_0247_URI).setCode("P"))
.andParameter("url", new UriType(CODE_SYSTEM_V2_0247_URI))
.withParameter(Parameters.class, "coding", new Coding().setSystem(system).setCode(code))
.andParameter("url", new UriType(system))
.execute();
String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
@ -128,7 +118,7 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour
@Test
public void validateCodeOperationOnCodeSystem_byCodingAndUrlWhereCodeSystemIsUnknown_returnsFalse() {
myCodeSystemProvider.myReturnCodeSystems = new ArrayList<>();
myCodeSystemProvider.setShouldThrowExceptionForResourceNotFound(false);
Parameters respParam = myClient
.operation()
@ -166,21 +156,21 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour
@Test
public void validateCodeOperationOnValueSet_byUrlAndSystem_usingBuiltInCodeSystems() {
myCodeSystemProvider.myReturnCodeSystems = new ArrayList<>();
myCodeSystemProvider.myReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/list-example-use-codes"));
myValueSetProvider.myReturnValueSets = new ArrayList<>();
myValueSetProvider.myReturnValueSets.add((ValueSet) new ValueSet().setId("ValueSet/list-example-codes"));
myValueSetProvider.myReturnParams = new Parameters();
myValueSetProvider.myReturnParams.addParameter("result", true);
myValueSetProvider.myReturnParams.addParameter("display", DISPLAY);
final String code = "alerts";
final String system = "http://terminology.hl7.org/CodeSystem/list-example-use-codes";
final String valueSetUrl = "http://hl7.org/fhir/ValueSet/list-example-codes";
Parameters params = new Parameters().addParameter("result", true).addParameter("display", DISPLAY);
setupValueSetValidateCode(valueSetUrl, system, code, params);
setupCodeSystemValidateCode(system, code, params);
Parameters respParam = myClient
.operation()
.onType(ValueSet.class)
.named(JpaConstants.OPERATION_VALIDATE_CODE)
.withParameter(Parameters.class, "code", new CodeType("alerts"))
.andParameter("system", new UriType("http://terminology.hl7.org/CodeSystem/list-example-use-codes"))
.andParameter("url", new UriType("http://hl7.org/fhir/ValueSet/list-example-codes"))
.withParameter(Parameters.class, "code", new CodeType(code))
.andParameter("system", new UriType(system))
.andParameter("url", new UriType(valueSetUrl))
.useHttpGet()
.execute();
@ -193,21 +183,20 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour
@Test
public void validateCodeOperationOnValueSet_byUrlSystemAndCode() {
myCodeSystemProvider.myReturnCodeSystems = new ArrayList<>();
myCodeSystemProvider.myReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/list-example-use-codes"));
myValueSetProvider.myReturnValueSets = new ArrayList<>();
myValueSetProvider.myReturnValueSets.add((ValueSet) new ValueSet().setId("ValueSet/list-example-codes"));
myValueSetProvider.myReturnParams = new Parameters();
myValueSetProvider.myReturnParams.addParameter("result", true);
myValueSetProvider.myReturnParams.addParameter("display", DISPLAY_BODY_MASS_INDEX);
final String code = CODE_BODY_MASS_INDEX;
final String system = "http://terminology.hl7.org/CodeSystem/list-example-use-codes";
final String valueSetUrl = "http://hl7.org/fhir/ValueSet/list-example-codes";
Parameters params = new Parameters().addParameter("result", true).addParameter("display", DISPLAY_BODY_MASS_INDEX);
setupValueSetValidateCode(valueSetUrl, system, code, params);
Parameters respParam = myClient
.operation()
.onType(ValueSet.class)
.named(JpaConstants.OPERATION_VALIDATE_CODE)
.withParameter(Parameters.class, "code", new CodeType(CODE_BODY_MASS_INDEX))
.andParameter("url", new UriType("https://loinc.org"))
.andParameter("system", new UriType("http://loinc.org"))
.withParameter(Parameters.class, "code", new CodeType(code))
.andParameter("url", new UriType(valueSetUrl))
.andParameter("system", new UriType(system))
.execute();
String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam);
@ -219,7 +208,7 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour
@Test
public void validateCodeOperationOnValueSet_byCodingAndUrlWhereValueSetIsUnknown_returnsFalse() {
myValueSetProvider.myReturnValueSets = new ArrayList<>();
myValueSetProvider.setShouldThrowExceptionForResourceNotFound(false);
Parameters respParam = myClient
.operation()
@ -238,70 +227,18 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour
" - Unknown or unusable ValueSet[" + UNKNOWN_VALUE_SYSTEM_URI + "]");
}
@SuppressWarnings("unused")
private static class MyCodeSystemProvider implements IResourceProvider {
private List<CodeSystem> myReturnCodeSystems;
private Parameters myReturnParams;
private void setupValueSetValidateCode(String theUrl, String theSystem, String theCode, IBaseParameters theResponseParams) {
ValueSet valueSet = myValueSetProvider.addTerminologyResource(theUrl);
myValueSetProvider.addTerminologyResource(theSystem);
myValueSetProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, valueSet.getUrl(), theCode, theResponseParams);
@Operation(name = "validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = StringType.class),
@OperationParam(name = "display", type = StringType.class)
})
public Parameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) IdType theId,
@OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl,
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay
) {
return myReturnParams;
}
@Search
public List<CodeSystem> find(@RequiredParam(name = "url") UriParam theUrlParam) {
assert myReturnCodeSystems != null;
return myReturnCodeSystems;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
// we currently do this because VersionSpecificWorkerContextWrapper has logic to infer the system when missing
// based on the ValueSet by calling ValidationSupportUtils#extractCodeSystemForCode.
valueSet.getCompose().addInclude().setSystem(theSystem);
}
@SuppressWarnings("unused")
private static class MyValueSetProvider implements IResourceProvider {
private Parameters myReturnParams;
private List<ValueSet> myReturnValueSets;
@Operation(name = "validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = StringType.class),
@OperationParam(name = "display", type = StringType.class)
})
public Parameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) IdType theId,
@OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl,
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
@OperationParam(name = "system", min = 0, max = 1) UriType theSystem,
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay,
@OperationParam(name = "valueSet") ValueSet theValueSet
) {
return myReturnParams;
}
@Search
public List<ValueSet> find(@RequiredParam(name = "url") UriParam theUrlParam) {
assert myReturnValueSets != null;
return myReturnValueSets;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return ValueSet.class;
}
private void setupCodeSystemValidateCode(String theUrl, String theCode, IBaseParameters theResponseParams) {
CodeSystem codeSystem = myCodeSystemProvider.addTerminologyResource(theUrl);
myCodeSystemProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, codeSystem.getUrl(), theCode, theResponseParams);
}
}

View File

@ -0,0 +1,261 @@
package ca.uhn.fhir.jpa.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.config.JpaConfig;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.test.utilities.validation.IValidationProviders;
import ca.uhn.fhir.test.utilities.validation.IValidationProvidersR4;
import ca.uhn.fhir.util.ClasspathUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Procedure;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import java.util.List;
import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_VALIDATE_CODE;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests resource validation with Remote Terminology bindings.
* To create a new test, you need to do 3 things:
* (1) the resource profile, if any custom one is needed should be stored in the FHIR repository
* (2) all the CodeSystem and ValueSet terminology resources need to be added to the corresponding resource provider.
* At the moment only placeholder CodeSystem/ValueSet resources are returned with id and url populated. For the moment
* there was no need to load the full resource, but that can be done if there is logic run which requires it.
* This is a minimal setup.
* (3) the Remote Terminology operation responses that are needed for the test need to be added to the corresponding
* resource provider. The intention is to record and use the responses of an actual terminology server
* e.g. <a href="https://r4.ontoserver.csiro.au/fhir/">OntoServer</a>.
* This is done as a result of the fact that unit test cannot always catch bugs which are introduced as a result of
* changes in the OntoServer or FHIR Validator library, or both.
* @see #setupValueSetValidateCode
* @see #setupCodeSystemValidateCode
* The responses are in Parameters resource format where issues is an OperationOutcome resource.
*/
public class ValidateWithRemoteTerminologyTest extends BaseResourceProviderR4Test {
private static final FhirContext ourCtx = FhirContext.forR4Cached();
@RegisterExtension
protected static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx);
private RemoteTerminologyServiceValidationSupport mySvc;
@Autowired
@Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN)
private ValidationSupportChain myValidationSupportChain;
private IValidationProviders.MyValidationProvider<CodeSystem> myCodeSystemProvider;
private IValidationProviders.MyValidationProvider<ValueSet> myValueSetProvider;
@BeforeEach
public void before() {
String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort();
mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl);
myValidationSupportChain.addValidationSupport(0, mySvc);
myCodeSystemProvider = new IValidationProvidersR4.MyCodeSystemProviderR4();
myValueSetProvider = new IValidationProvidersR4.MyValueSetProviderR4();
ourRestfulServerExtension.registerProvider(myCodeSystemProvider);
ourRestfulServerExtension.registerProvider(myValueSetProvider);
}
@AfterEach
public void after() {
myValidationSupportChain.removeValidationSupport(mySvc);
ourRestfulServerExtension.getRestfulServer().getInterceptorService().unregisterAllInterceptors();
ourRestfulServerExtension.unregisterProvider(myCodeSystemProvider);
ourRestfulServerExtension.unregisterProvider(myValueSetProvider);
}
@Test
public void validate_withProfileWithValidCodesFromAllBindingTypes_returnsNoErrors() {
// setup
final StructureDefinition profileEncounter = ClasspathUtil.loadResource(ourCtx, StructureDefinition.class, "validation/encounter/profile-encounter-custom.json");
myClient.update().resource(profileEncounter).execute();
final String statusCode = "planned";
final String classCode = "IMP";
final String identifierTypeCode = "VN";
final String statusSystem = "http://hl7.org/fhir/encounter-status"; // implied system
final String classSystem = "http://terminology.hl7.org/CodeSystem/v3-ActCode";
final String identifierTypeSystem = "http://terminology.hl7.org/CodeSystem/v2-0203";
setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/encounter-status", "http://hl7.org/fhir/encounter-status", statusCode, "validation/encounter/validateCode-ValueSet-encounter-status.json");
setupValueSetValidateCode("http://terminology.hl7.org/ValueSet/v3-ActEncounterCode", "http://terminology.hl7.org/CodeSystem/v3-ActCode", classCode, "validation/encounter/validateCode-ValueSet-v3-ActEncounterCode.json");
setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/identifier-type", "http://hl7.org/fhir/identifier-type", identifierTypeCode, "validation/encounter/validateCode-ValueSet-identifier-type.json");
setupCodeSystemValidateCode(statusSystem, statusCode, "validation/encounter/validateCode-CodeSystem-encounter-status.json");
setupCodeSystemValidateCode(classSystem, classCode, "validation/encounter/validateCode-CodeSystem-v3-ActCode.json");
setupCodeSystemValidateCode(identifierTypeSystem, identifierTypeCode, "validation/encounter/validateCode-CodeSystem-v2-0203.json");
Encounter encounter = new Encounter();
encounter.getMeta().addProfile("http://example.ca/fhir/StructureDefinition/profile-encounter");
// required binding
encounter.setStatus(Encounter.EncounterStatus.fromCode(statusCode));
// preferred binding
encounter.getClass_()
.setSystem(classSystem)
.setCode(classCode)
.setDisplay("inpatient encounter");
// extensible binding
encounter.addIdentifier()
.getType().addCoding()
.setSystem(identifierTypeSystem)
.setCode(identifierTypeCode)
.setDisplay("Visit number");
// execute
List<String> errors = getValidationErrors(encounter);
// verify
assertThat(errors).isEmpty();
}
@Test
public void validate_withInvalidCode_returnsErrors() {
// setup
final String statusCode = "final";
final String code = "10xx";
final String statusSystem = "http://hl7.org/fhir/observation-status";
final String loincSystem = "http://loinc.org";
final String system = "http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM";
setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/observation-status", statusSystem, statusCode, "validation/observation/validateCode-ValueSet-observation-status.json");
setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/observation-codes", loincSystem, statusCode, "validation/observation/validateCode-ValueSet-codes.json");
setupCodeSystemValidateCode(statusSystem, statusCode, "validation/observation/validateCode-CodeSystem-observation-status.json");
setupCodeSystemValidateCode(system, code, "validation/observation/validateCode-CodeSystem-ICD9CM.json");
Observation obs = new Observation();
obs.setStatus(Observation.ObservationStatus.fromCode(statusCode));
obs.getCode().addCoding().setCode(code).setSystem(system);
// execute
List<String> errors = getValidationErrors(obs);
assertThat(errors).hasSize(1);
// verify
assertThat(errors.get(0))
.contains("Unknown code '10xx' in the CodeSystem 'http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM");
}
@Test
public void validate_withProfileWithInvalidCode_returnsErrors() {
// setup
String profile = "http://example.ca/fhir/StructureDefinition/profile-procedure";
StructureDefinition profileProcedure = ClasspathUtil.loadResource(myFhirContext, StructureDefinition.class, "validation/procedure/profile-procedure.json");
myClient.update().resource(profileProcedure).execute();
final String statusCode = "completed";
final String procedureCode1 = "417005";
final String procedureCode2 = "xx417005";
final String statusSystem = "http://hl7.org/fhir/event-status";
final String snomedSystem = "http://snomed.info/sct";
setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/event-status", statusSystem, statusCode, "validation/procedure/validateCode-ValueSet-event-status.json");
setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/procedure-code", snomedSystem, procedureCode1, "validation/procedure/validateCode-ValueSet-procedure-code-valid.json");
setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/procedure-code", snomedSystem, procedureCode2, "validation/procedure/validateCode-ValueSet-procedure-code-invalid.json");
setupCodeSystemValidateCode(statusSystem, statusCode, "validation/procedure/validateCode-CodeSystem-event-status.json");
setupCodeSystemValidateCode(snomedSystem, procedureCode1, "validation/procedure/validateCode-CodeSystem-snomed-valid.json");
setupCodeSystemValidateCode(snomedSystem, procedureCode2, "validation/procedure/validateCode-CodeSystem-snomed-invalid.json");
Procedure procedure = new Procedure();
procedure.setSubject(new Reference("Patient/P1"));
procedure.setStatus(Procedure.ProcedureStatus.fromCode(statusCode));
procedure.getCode().addCoding().setSystem(snomedSystem).setCode(procedureCode1);
procedure.getCode().addCoding().setSystem(snomedSystem).setCode(procedureCode2);
procedure.getMeta().addProfile(profile);
// execute
List<String> errors = getValidationErrors(procedure);
// TODO: there is currently some duplication in the errors returned. This needs to be investigated and fixed.
// assertThat(errors).hasSize(1);
// verify
// note that we're not selecting an explicit versions (using latest) so the message verification does not include it.
assertThat(StringUtils.join("", errors))
.contains("Unknown code 'xx417005' in the CodeSystem 'http://snomed.info/sct'")
.doesNotContain("The provided code 'http://snomed.info/sct#xx417005' was not found in the value set 'http://hl7.org/fhir/ValueSet/procedure-code")
.doesNotContain("http://snomed.info/sct#417005");
}
@Test
public void validate_withProfileWithSlicingWithValidCode_returnsNoErrors() {
// setup
String profile = "http://example.ca/fhir/StructureDefinition/profile-procedure-with-slicing";
StructureDefinition profileProcedure = ClasspathUtil.loadResource(myFhirContext, StructureDefinition.class, "validation/procedure/profile-procedure-slicing.json");
myClient.update().resource(profileProcedure).execute();
final String statusCode = "completed";
final String procedureCode = "no-procedure-info";
final String statusSystem = "http://hl7.org/fhir/event-status";
final String snomedSystem = "http://snomed.info/sct";
final String absentUnknownSystem = "http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips";
setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/event-status", statusSystem, statusCode, "validation/procedure/validateCode-ValueSet-event-status.json");
setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/procedure-code", snomedSystem, procedureCode, "validation/procedure/validateCode-ValueSet-procedure-code-invalid-slice.json");
setupValueSetValidateCode("http://hl7.org/fhir/uv/ips/ValueSet/absent-or-unknown-procedures-uv-ips", absentUnknownSystem, procedureCode, "validation/procedure/validateCode-ValueSet-absent-or-unknown-procedure.json");
setupCodeSystemValidateCode(statusSystem, statusCode, "validation/procedure/validateCode-CodeSystem-event-status.json");
setupCodeSystemValidateCode(absentUnknownSystem, procedureCode, "validation/procedure/validateCode-CodeSystem-absent-or-unknown.json");
Procedure procedure = new Procedure();
procedure.setSubject(new Reference("Patient/P1"));
procedure.setStatus(Procedure.ProcedureStatus.fromCode(statusCode));
procedure.getCode().addCoding().setSystem(absentUnknownSystem).setCode(procedureCode);
procedure.getMeta().addProfile(profile);
// execute
List<String> errors = getValidationErrors(procedure);
assertThat(errors).hasSize(0);
}
private void setupValueSetValidateCode(String theUrl, String theSystem, String theCode, String theTerminologyResponseFile) {
ValueSet valueSet = myValueSetProvider.addTerminologyResource(theUrl);
myCodeSystemProvider.addTerminologyResource(theSystem);
myValueSetProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, valueSet.getUrl(), theCode, ourCtx, theTerminologyResponseFile);
// we currently do this because VersionSpecificWorkerContextWrapper has logic to infer the system when missing
// based on the ValueSet by calling ValidationSupportUtils#extractCodeSystemForCode.
valueSet.getCompose().addInclude().setSystem(theSystem);
// you will notice each of these calls require also a call to setupCodeSystemValidateCode
// that is necessary because VersionSpecificWorkerContextWrapper#validateCodeInValueSet
// which also attempts a validateCode against the CodeSystem after the validateCode against the ValueSet
}
private void setupCodeSystemValidateCode(String theUrl, String theCode, String theTerminologyResponseFile) {
CodeSystem codeSystem = myCodeSystemProvider.addTerminologyResource(theUrl);
myCodeSystemProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, codeSystem.getUrl(), theCode, ourCtx, theTerminologyResponseFile);
}
private List<String> getValidationErrors(IBaseResource theResource) {
MethodOutcome resultProcedure = myClient.validate().resource(theResource).execute();
OperationOutcome operationOutcome = (OperationOutcome) resultProcedure.getOperationOutcome();
return operationOutcome.getIssue().stream()
.filter(issue -> issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR)
.map(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics)
.toList();
}
}

View File

@ -31,7 +31,7 @@
<logger name="ca.uhn.fhir.jpa.bulk" level="info"/>
<!-- more debugging -->
<!--
<!--
<logger name="org.elasticsearch.client" level="trace"/>
<logger name="org.hibernate.search.elasticsearch.request" level="TRACE"/>
<logger name="ca.uhn.fhir.jpa.model.search" level="debug"/>
@ -39,7 +39,7 @@
<logger name="org.hibernate.search" level="debug"/>
<logger name="org.hibernate.search.query" level="TRACE"/>
<logger name="org.hibernate.search.elasticsearch.request" level="TRACE"/>
-->
-->
<!-- See https://docs.jboss.org/hibernate/stable/search/reference/en-US/html_single/#backend-lucene-io-writer-infostream for lucene logging
<logger name="org.hibernate.search.backend.lucene.infostream" level="TRACE"/> -->

View File

@ -0,0 +1,49 @@
{
"resourceType": "StructureDefinition",
"id": "profile-encounter",
"url": "http://example.ca/fhir/StructureDefinition/profile-encounter",
"version": "0.11.0",
"name": "EncounterProfile",
"title": "Encounter Profile",
"status": "active",
"date": "2022-10-15T12:00:00+00:00",
"publisher": "Example Organization",
"fhirVersion": "4.0.1",
"kind": "resource",
"abstract": false,
"type": "Encounter",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/Encounter",
"derivation": "constraint",
"differential": {
"element": [
{
"id": "Encounter.identifier.type.coding",
"path": "Encounter.identifier.type.coding",
"min": 1,
"max": "1",
"mustSupport": true
},
{
"id": "Encounter.identifier.type.coding.system",
"path": "Encounter.identifier.type.coding.system",
"min": 1,
"fixedUri": "http://terminology.hl7.org/CodeSystem/v2-0203",
"mustSupport": true
},
{
"id": "Encounter.identifier.type.coding.code",
"path": "Encounter.identifier.type.coding.code",
"min": 1,
"fixedCode": "VN",
"mustSupport": true
},
{
"id": "Encounter.identifier.type.coding.display",
"path": "Encounter.identifier.type.coding.display",
"min": 1,
"fixedString": "Visit number",
"mustSupport": true
}
]
}
}

View File

@ -0,0 +1,59 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "planned"
},
{
"name": "system",
"valueUri": "http://hl7.org/fhir/encounter-status"
},
{
"name": "version",
"valueString": "5.0.0-ballot"
},
{
"name": "display",
"valueString": "Planned"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to trial-use CodeSystem http://hl7.org/fhir/encounter-status|5.0.0-ballot"
}
},
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to draft CodeSystem http://hl7.org/fhir/encounter-status|5.0.0-ballot"
}
}
]
}
}
]
}

View File

@ -0,0 +1,25 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "VN"
},
{
"name": "system",
"valueUri": "http://terminology.hl7.org/CodeSystem/v2-0203"
},
{
"name": "version",
"valueString": "3.0.0"
},
{
"name": "display",
"valueString": "Visit number"
}
]
}

View File

@ -0,0 +1,46 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "IMP"
},
{
"name": "system",
"valueUri": "http://terminology.hl7.org/CodeSystem/v3-ActCode"
},
{
"name": "version",
"valueString": "2018-08-12"
},
{
"name": "display",
"valueString": "inpatient encounter"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to draft CodeSystem http://terminology.hl7.org/CodeSystem/v3-ActCode|2018-08-12"
}
}
]
}
}
]
}

View File

@ -0,0 +1,59 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "planned"
},
{
"name": "system",
"valueUri": "http://hl7.org/fhir/encounter-status"
},
{
"name": "version",
"valueString": "5.0.0-ballot"
},
{
"name": "display",
"valueString": "Planned"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to trial-use CodeSystem http://hl7.org/fhir/encounter-status|5.0.0-ballot"
}
},
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to draft CodeSystem http://hl7.org/fhir/encounter-status|5.0.0-ballot"
}
}
]
}
}
]
}

View File

@ -0,0 +1,52 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": false
},
{
"name": "code",
"valueCode": "VN"
},
{
"name": "system",
"valueUri": "http://terminology.hl7.org/CodeSystem/v2-0203"
},
{
"name": "version",
"valueString": "3.0.0"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "code-invalid",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "not-in-vs"
}
],
"text": "The provided code 'http://terminology.hl7.org/CodeSystem/v2-0203#VN' was not found in the value set 'http://hl7.org/fhir/ValueSet/identifier-type|5.0.0-ballot'"
},
"location": [
"code"
],
"expression": [
"code"
]
}
]
}
},
{
"name": "message",
"valueString": "The provided code 'http://terminology.hl7.org/CodeSystem/v2-0203#VN' was not found in the value set 'http://hl7.org/fhir/ValueSet/identifier-type|5.0.0-ballot'"
}
]
}

View File

@ -0,0 +1,59 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "IMP"
},
{
"name": "system",
"valueUri": "http://terminology.hl7.org/CodeSystem/v3-ActCode"
},
{
"name": "version",
"valueString": "2018-08-12"
},
{
"name": "display",
"valueString": "inpatient encounter"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to trial-use ValueSet http://terminology.hl7.org/ValueSet/v3-ActEncounterCode|2014-03-26"
}
},
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to draft CodeSystem http://terminology.hl7.org/CodeSystem/v3-ActCode|2018-08-12"
}
}
]
}
}
]
}

View File

@ -0,0 +1,48 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": false
},
{
"name": "code",
"valueCode": "10xx"
},
{
"name": "system",
"valueUri": "http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "code-invalid",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "invalid-code"
}
],
"text": "Unknown code '10xx' in the CodeSystem 'http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM' version '0.1.0'"
},
"location": [
"code"
],
"expression": [
"code"
]
}
]
}
},
{
"name": "message",
"valueString": "Unknown code '10xx' in the CodeSystem 'http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM' version '0.1.0'"
}
]
}

View File

@ -0,0 +1,25 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "final"
},
{
"name": "system",
"valueUri": "http://hl7.org/fhir/observation-status"
},
{
"name": "version",
"valueString": "5.0.0-ballot"
},
{
"name": "display",
"valueString": "Final"
}
]
}

View File

@ -0,0 +1,48 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": false
},
{
"name": "code",
"valueCode": "10xx"
},
{
"name": "system",
"valueUri": "http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "code-invalid",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "not-in-vs"
}
],
"text": "The provided code 'http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM#10xx' was not found in the value set 'http://hl7.org/fhir/ValueSet/observation-codes|5.0.0-ballot'"
},
"location": [
"code"
],
"expression": [
"code"
]
}
]
}
},
{
"name": "message",
"valueString": "The provided code 'http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM#10xx' was not found in the value set 'http://hl7.org/fhir/ValueSet/observation-codes|5.0.0-ballot'"
}
]
}

View File

@ -0,0 +1,25 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "final"
},
{
"name": "system",
"valueUri": "http://hl7.org/fhir/observation-status"
},
{
"name": "version",
"valueString": "5.0.0-ballot"
},
{
"name": "display",
"valueString": "Final"
}
]
}

View File

@ -0,0 +1,79 @@
{
"resourceType": "StructureDefinition",
"id": "profile-procedure-with-slicing",
"url": "http://example.ca/fhir/StructureDefinition/profile-procedure-with-slicing",
"version": "0.11.0",
"name": "ProcedureProfile",
"title": "Procedure Profile",
"status": "active",
"date": "2022-10-15T12:00:00+00:00",
"publisher": "Example Organization",
"fhirVersion": "4.0.1",
"kind": "resource",
"abstract": false,
"type": "Procedure",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/Procedure",
"derivation": "constraint",
"differential": {
"element": [
{
"id": "Procedure.code.coding",
"path": "Procedure.code.coding",
"slicing": {
"discriminator": [
{
"type": "pattern",
"path": "$this"
}
],
"description": "Discriminated by the bound value set",
"rules": "open"
},
"mustSupport": true,
"binding": {
"strength": "preferred",
"valueSet": "http://hl7.org/fhir/ValueSet/procedure-code"
}
},
{
"id": "Procedure.code.coding.display.extension:translation",
"path": "Procedure.code.coding.display.extension",
"sliceName": "translation"
},
{
"id": "Procedure.code.coding.display.extension:translation.extension",
"path": "Procedure.code.coding.display.extension.extension",
"min": 2
},
{
"id": "Procedure.code.coding:absentOrUnknownProcedure",
"path": "Procedure.code.coding",
"sliceName": "absentOrUnknownProcedure",
"short": "Optional slice for representing a code for absent problem or for unknown procedure",
"definition": "Code representing the statement \"absent problem\" or the statement \"procedures unknown\"",
"mustSupport": true,
"binding": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName",
"valueString": "absentOrUnknownProcedure"
}
],
"strength": "required",
"description": "A code to identify absent or unknown procedures",
"valueSet": "http://hl7.org/fhir/uv/ips/ValueSet/absent-or-unknown-procedures-uv-ips"
}
},
{
"id": "Procedure.code.coding:absentOrUnknownProcedure.display.extension:translation",
"path": "Procedure.code.coding.display.extension",
"sliceName": "translation"
},
{
"id": "Procedure.code.coding:absentOrUnknownProcedure.display.extension:translation.extension",
"path": "Procedure.code.coding.display.extension.extension",
"min": 2
}
]
}
}

View File

@ -0,0 +1,50 @@
{
"resourceType": "StructureDefinition",
"id": "profile-procedure",
"url": "http://example.ca/fhir/StructureDefinition/profile-procedure",
"version": "0.11.0",
"name": "ProcedureProfile",
"title": "Procedure Profile",
"status": "active",
"date": "2022-10-15T12:00:00+00:00",
"publisher": "Example Organization",
"fhirVersion": "4.0.1",
"kind": "resource",
"abstract": false,
"type": "Procedure",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/Procedure",
"derivation": "constraint",
"differential": {
"element": [
{
"id": "Procedure.code.coding",
"path": "Procedure.code.coding",
"slicing": {
"discriminator": [
{
"type": "pattern",
"path": "$this"
}
],
"description": "Discriminated by the bound value set",
"rules": "open"
},
"mustSupport": true,
"binding": {
"strength": "preferred",
"valueSet": "http://hl7.org/fhir/ValueSet/procedure-code"
}
},
{
"id": "Procedure.code.coding.display.extension:translation",
"path": "Procedure.code.coding.display.extension",
"sliceName": "translation"
},
{
"id": "Procedure.code.coding.display.extension:translation.extension",
"path": "Procedure.code.coding.display.extension.extension",
"min": 2
}
]
}
}

View File

@ -0,0 +1,46 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "no-procedure-info"
},
{
"name": "system",
"valueUri": "http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips"
},
{
"name": "version",
"valueString": "1.1.0"
},
{
"name": "display",
"valueString": "No information about past history of procedures"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to trial-use CodeSystem http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips|1.1.0"
}
}
]
}
}
]
}

View File

@ -0,0 +1,59 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "completed"
},
{
"name": "system",
"valueUri": "http://hl7.org/fhir/event-status"
},
{
"name": "version",
"valueString": "5.0.0-ballot"
},
{
"name": "display",
"valueString": "Completed"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to trial-use CodeSystem http://hl7.org/fhir/event-status|5.0.0-ballot"
}
},
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to experimental CodeSystem http://hl7.org/fhir/event-status|5.0.0-ballot"
}
}
]
}
}
]
}

View File

@ -0,0 +1,48 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": false
},
{
"name": "code",
"valueCode": "xx417005"
},
{
"name": "system",
"valueUri": "http://snomed.info/sct"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "code-invalid",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "invalid-code"
}
],
"text": "Unknown code 'xx417005' in the CodeSystem 'http://snomed.info/sct' version 'http://snomed.info/sct/32506021000036107/version/20241031'"
},
"location": [
"code"
],
"expression": [
"code"
]
}
]
}
},
{
"name": "message",
"valueString": "Unknown code 'xx417005' in the CodeSystem 'http://snomed.info/sct' version 'http://snomed.info/sct/32506021000036107/version/20241031'"
}
]
}

View File

@ -0,0 +1,25 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "417005"
},
{
"name": "system",
"valueUri": "http://snomed.info/sct"
},
{
"name": "version",
"valueString": "http://snomed.info/sct/32506021000036107/version/20241031"
},
{
"name": "display",
"valueString": "Hospital re-admission"
}
]
}

View File

@ -0,0 +1,59 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "no-procedure-info"
},
{
"name": "system",
"valueUri": "http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips"
},
{
"name": "version",
"valueString": "1.1.0"
},
{
"name": "display",
"valueString": "No information about past history of procedures"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to trial-use CodeSystem http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips|1.1.0"
}
},
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to trial-use ValueSet http://hl7.org/fhir/uv/ips/ValueSet/absent-or-unknown-procedures-uv-ips|1.1.0"
}
}
]
}
}
]
}

View File

@ -0,0 +1,25 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "final"
},
{
"name": "system",
"valueUri": "http://hl7.org/fhir/procedure-status"
},
{
"name": "version",
"valueString": "5.0.0-ballot"
},
{
"name": "display",
"valueString": "Final"
}
]
}

View File

@ -0,0 +1,48 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": false
},
{
"name": "code",
"valueCode": "no-procedure-info"
},
{
"name": "system",
"valueUri": "http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "code-invalid",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "not-in-vs"
}
],
"text": "The provided code 'http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips#no-procedure-info' was not found in the value set 'http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot'"
},
"location": [
"code"
],
"expression": [
"code"
]
}
]
}
},
{
"name": "message",
"valueString": "The provided code 'http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips#no-procedure-info' was not found in the value set 'http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot'"
}
]
}

View File

@ -0,0 +1,67 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": false
},
{
"name": "code",
"valueCode": "xx417005"
},
{
"name": "system",
"valueUri": "http://snomed.info/sct"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "code-invalid",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "not-in-vs"
}
],
"text": "The provided code 'http://snomed.info/sct#xx417005' was not found in the value set 'http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot'"
},
"location": [
"code"
],
"expression": [
"code"
]
},
{
"severity": "error",
"code": "code-invalid",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "invalid-code"
}
],
"text": "Unknown code 'xx417005' in the CodeSystem 'http://snomed.info/sct' version 'http://snomed.info/sct/32506021000036107/version/20241031'"
},
"location": [
"code"
],
"expression": [
"code"
]
}
]
}
},
{
"name": "message",
"valueString": "Unknown code 'xx417005' in the CodeSystem 'http://snomed.info/sct' version 'http://snomed.info/sct/32506021000036107/version/20241031'; The provided code 'http://snomed.info/sct#xx417005' was not found in the value set 'http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot'"
}
]
}

View File

@ -0,0 +1,59 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "result",
"valueBoolean": true
},
{
"name": "code",
"valueCode": "417005"
},
{
"name": "system",
"valueUri": "http://snomed.info/sct"
},
{
"name": "version",
"valueString": "http://snomed.info/sct/32506021000036107/version/20241031"
},
{
"name": "display",
"valueString": "Hospital re-admission"
},
{
"name": "issues",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to draft ValueSet http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot"
}
},
{
"severity": "information",
"code": "business-rule",
"details": {
"coding": [
{
"system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
"code": "status-check"
}
],
"text": "Reference to experimental ValueSet http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot"
}
}
]
}
}
]
}

View File

@ -2,19 +2,29 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportJobSchedulingHelper;
import ca.uhn.fhir.jpa.dao.TestDaoSearch;
import ca.uhn.fhir.jpa.search.BaseSourceSearchParameterTestCases;
import ca.uhn.fhir.jpa.search.CompositeSearchParameterTestCases;
import ca.uhn.fhir.jpa.search.QuantitySearchParameterTestCases;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.test.BaseJpaTest;
import ca.uhn.fhir.jpa.test.config.TestR4Config;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.storage.test.BaseDateSearchDaoTests;
import ca.uhn.fhir.storage.test.DaoTestDataBuilder;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.PractitionerRole;
import org.hl7.fhir.r4.model.Reference;
@ -32,15 +42,15 @@ import org.springframework.transaction.PlatformTransactionManager;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
TestR4Config.class,
DaoTestDataBuilder.Config.class,
TestDaoSearch.Config.class
})
public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest {
public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest
implements ILuceneSearchR4Test {
FhirContext myFhirContext = FhirContext.forR4Cached();
@Autowired
PlatformTransactionManager myTxManager;
@ -53,6 +63,19 @@ public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest {
@Qualifier("myObservationDaoR4")
IFhirResourceDao<Observation> myObservationDao;
@Autowired
@Qualifier("myPatientDaoR4")
protected IFhirResourceDaoPatient<Patient> myPatientDao;
@Autowired
private IFhirSystemDao<Bundle, Meta> mySystemDao;
@Autowired
private IResourceReindexingSvc myResourceReindexingSvc;
@Autowired
protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
@Autowired
protected ISearchParamRegistry mySearchParamRegistry;
@Autowired
private IBulkDataExportJobSchedulingHelper myBulkDataScheduleHelper;
@Autowired
IFhirResourceDao<Practitioner> myPractitionerDao;
@Autowired
IFhirResourceDao<PractitionerRole> myPractitionerRoleDao;
@ -60,6 +83,7 @@ public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest {
// todo mb create an extension to restore via clone or xstream + BeanUtils.copyProperties().
@BeforeEach
void setUp() {
purgeDatabase(myStorageSettings, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataScheduleHelper);
myStorageSettings.setAdvancedHSearchIndexing(true);
}
@ -79,6 +103,11 @@ public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest {
return myTxManager;
}
@Override
public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
@Nested
public class DateSearchTests extends BaseDateSearchDaoTests {
@Override

View File

@ -0,0 +1,317 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.util.DateRangeUtil;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public interface ILuceneSearchR4Test {
DaoRegistry getDaoRegistry();
@SuppressWarnings("rawtypes")
private IFhirResourceDao getResourceDao(String theResourceType) {
return getDaoRegistry()
.getResourceDao(theResourceType);
}
void runInTransaction(Runnable theRunnable);
@Test
default void testNoOpUpdateDoesNotModifyLastUpdated() throws InterruptedException {
IFhirResourceDao<Patient> patientDao = getResourceDao("Patient");
Patient patient = new Patient();
patient.getNameFirstRep().setFamily("graham").addGiven("gary");
patient = (Patient) patientDao.create(patient).getResource();
Date originalLastUpdated = patient.getMeta().getLastUpdated();
patient = (Patient) patientDao.update(patient).getResource();
Date newLastUpdated = patient.getMeta().getLastUpdated();
assertThat(originalLastUpdated).isEqualTo(newLastUpdated);
}
@Test
default void luceneSearch_forTagsAndLastUpdated_shouldReturn() {
// setup
SystemRequestDetails requestDeatils = new SystemRequestDetails();
String system = "http://fhir";
String code = "cv";
Date start = Date.from(Instant.now().minus(1, ChronoUnit.SECONDS).truncatedTo(ChronoUnit.SECONDS));
Date end = Date.from(Instant.now().plus(10, ChronoUnit.SECONDS).truncatedTo(ChronoUnit.SECONDS));
@SuppressWarnings("unchecked")
IFhirResourceDao<Patient> patientDao = getResourceDao("Patient");
// create a patient with some tag
Patient patient = new Patient();
patient.getMeta()
.addTag(system, code, "");
patient.addName().addGiven("homer")
.setFamily("simpson");
patient.addAddress()
.setCity("springfield")
.addLine("742 evergreen terrace");
Long id = patientDao.create(patient, requestDeatils).getId().toUnqualifiedVersionless().getIdPartAsLong();
// create base search map
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true);
TokenOrListParam goldenRecordStatusToken = new TokenOrListParam(system, code);
map.add(Constants.PARAM_TAG, goldenRecordStatusToken);
DateRangeParam lastUpdated = DateRangeUtil.narrowDateRange(map.getLastUpdated(), start, end);
map.setLastUpdated(lastUpdated);
runInTransaction(() -> {
Stream<JpaPid> stream;
List<JpaPid> list;
Optional<JpaPid> first;
// tag search only; should return our resource
map.setLastUpdated(null);
stream = patientDao.searchForIdStream(map, new SystemRequestDetails(), null);
list = stream.toList();
assertEquals(1, list.size());
first = list.stream().findFirst();
assertTrue(first.isPresent());
assertEquals(id, first.get().getId());
// last updated search only; should return our resource
map.setLastUpdated(lastUpdated);
map.remove(Constants.PARAM_TAG);
stream = patientDao.searchForIdStream(map, new SystemRequestDetails(), null);
list = stream.toList();
assertEquals(1, list.size());
first = list.stream().findFirst();
assertTrue(first.isPresent());
assertEquals(id, first.get().getId());
// both last updated and tags; should return our resource
map.add(Constants.PARAM_TAG, goldenRecordStatusToken);
stream = patientDao.searchForIdStream(map, new SystemRequestDetails(), null);
list = stream.toList();
assertEquals(1, list.size());
first = list.stream().findFirst();
assertTrue(first.isPresent());
assertEquals(id, first.get().getId());
});
}
@Test
default void searchLuceneAndJPA_withLuceneMatchingButJpaNot_returnsNothing() {
// setup
int numToCreate = 2 * SearchBuilder.getMaximumPageSize() + 10;
SystemRequestDetails requestDetails = new SystemRequestDetails();
@SuppressWarnings("unchecked")
IFhirResourceDao<Patient> patientDao = getResourceDao("Patient");
// create resources
for (int i = 0; i < numToCreate; i++) {
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier()
.setSystem("http://fhir.com")
.setValue("ZYX");
patient.getText().setDivAsString("<div>ABC</div>");
patientDao.create(patient, requestDetails);
}
// test
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.setSearchTotalMode(SearchTotalModeEnum.ACCURATE);
TokenAndListParam tokenAndListParam = new TokenAndListParam();
tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true")));
map.add("active", tokenAndListParam);
map.add(Constants.PARAM_TEXT, new StringParam("ABC"));
map.add("identifier", new TokenParam(null, "not found"));
IBundleProvider provider = patientDao.search(map, requestDetails);
// verify
assertEquals(0, provider.getAllResources().size());
}
@Test
default void testLuceneNarrativeSearchQueryIntersectingJpaQuery() {
final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10;
List<String> expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate);
SystemRequestDetails requestDetails = new SystemRequestDetails();
@SuppressWarnings("unchecked")
IFhirResourceDao<Patient> patientDao = getResourceDao("Patient");
// create active and non-active patients with the same narrative
for (int i = 0; i < numberOfPatientsToCreate; i++) {
Patient activePatient = new Patient();
activePatient.getText().setDivAsString("<div>AAAS<p>FOO</p> CCC </div>");
activePatient.setActive(true);
String patientId = patientDao.create(activePatient, requestDetails).getId().toUnqualifiedVersionless().getIdPart();
expectedActivePatientIds.add(patientId);
Patient nonActivePatient = new Patient();
nonActivePatient.getText().setDivAsString("<div>AAAS<p>FOO</p> CCC </div>");
nonActivePatient.setActive(false);
patientDao.create(nonActivePatient, requestDetails);
}
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true);
TokenAndListParam tokenAndListParam = new TokenAndListParam();
tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true")));
map.add("active", tokenAndListParam);
map.add(Constants.PARAM_TEXT, new StringParam("AAAS"));
IBundleProvider searchResultBundle = patientDao.search(map, requestDetails);
List<String> resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds();
assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds);
}
@Test
default void testLuceneContentSearchQueryIntersectingJpaQuery() {
final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10;
final String patientFamilyName = "Flanders";
List<String> expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate);
SystemRequestDetails requestDetails = new SystemRequestDetails();
@SuppressWarnings("unchecked")
IFhirResourceDao<Patient> patientDao = getResourceDao("Patient");
// create active and non-active patients with the same narrative
for (int i = 0; i < numberOfPatientsToCreate; i++) {
Patient activePatient = new Patient();
activePatient.addName().setFamily(patientFamilyName);
activePatient.setActive(true);
String patientId = patientDao.create(activePatient, requestDetails).getId().toUnqualifiedVersionless().getIdPart();
expectedActivePatientIds.add(patientId);
Patient nonActivePatient = new Patient();
nonActivePatient.addName().setFamily(patientFamilyName);
nonActivePatient.setActive(false);
patientDao.create(nonActivePatient, requestDetails);
}
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true);
TokenAndListParam tokenAndListParam = new TokenAndListParam();
tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true")));
map.add("active", tokenAndListParam);
map.add(Constants.PARAM_CONTENT, new StringParam(patientFamilyName));
IBundleProvider searchResultBundle = patientDao.search(map, requestDetails);
List<String> resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds();
assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds);
}
@Test
default void searchLuceneAndJPA_withLuceneBroadAndJPASearchNarrow_returnsFoundResults() {
// setup
int numToCreate = 2 * SearchBuilder.getMaximumPageSize() + 10;
String identifierToFind = "bcde";
SystemRequestDetails requestDetails = new SystemRequestDetails();
@SuppressWarnings("unchecked")
IFhirResourceDao<Patient> patientDao = getResourceDao("Patient");
// create patients
for (int i = 0; i < numToCreate; i++) {
Patient patient = new Patient();
patient.setActive(true);
String identifierVal = i == numToCreate - 10 ? identifierToFind:
"abcd";
patient.addIdentifier()
.setSystem("http://fhir.com")
.setValue(identifierVal);
patient.getText().setDivAsString(
"<div>FINDME</div>"
);
patientDao.create(patient, requestDetails);
}
// test
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.setSearchTotalMode(SearchTotalModeEnum.ACCURATE);
TokenAndListParam tokenAndListParam = new TokenAndListParam();
tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true")));
map.add("active", tokenAndListParam);
map.add(Constants.PARAM_TEXT, new StringParam("FINDME"));
map.add("identifier", new TokenParam(null, identifierToFind));
IBundleProvider provider = patientDao.search(map, requestDetails);
// verify
List<String> ids = provider.getAllResourceIds();
assertEquals(1, ids.size());
}
@Test
default void testSearchNarrativeWithLuceneSearch() {
final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10;
List<String> expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate);
SystemRequestDetails requestDetails = new SystemRequestDetails();
@SuppressWarnings("unchecked")
IFhirResourceDao<Patient> patientDao = getResourceDao("Patient");
for (int i = 0; i < numberOfPatientsToCreate; i++) {
Patient patient = new Patient();
patient.getText().setDivAsString("<div>AAAS<p>FOO</p> CCC </div>");
expectedActivePatientIds.add(patientDao.create(patient, requestDetails).getId().toUnqualifiedVersionless().getIdPart());
}
{
Patient patient = new Patient();
patient.getText().setDivAsString("<div>AAAB<p>FOO</p> CCC </div>");
patientDao.create(patient, requestDetails);
}
{
Patient patient = new Patient();
patient.getText().setDivAsString("<div>ZZYZXY</div>");
patientDao.create(patient, requestDetails);
}
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true);
map.add(Constants.PARAM_TEXT, new StringParam("AAAS"));
IBundleProvider searchResultBundle = patientDao.search(map, requestDetails);
List<String> resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds();
assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds);
}
}

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.CompositeSearchIndexData;
import ca.uhn.fhir.jpa.model.search.DateSearchIndexData;
import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData;
@ -55,7 +56,7 @@ class ExtendedHSearchIndexExtractorTest implements ITestDataBuilder.WithSupport
ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams("Observation", ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
ExtendedHSearchIndexExtractor extractor = new ExtendedHSearchIndexExtractor(
myJpaStorageSettings, myFhirContext, activeSearchParams, mySearchParamExtractor);
ExtendedHSearchIndexData indexData = extractor.extract(new Observation(), extractedParams);
ExtendedHSearchIndexData indexData = extractor.extract(new Observation(), new ResourceTable(), extractedParams);
// validate
Set<CompositeSearchIndexData> spIndexData = indexData.getSearchParamComposites().get("component-code-value-concept");
@ -78,14 +79,13 @@ class ExtendedHSearchIndexExtractorTest implements ITestDataBuilder.WithSupport
ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams("Patient", ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
ExtendedHSearchIndexExtractor extractor = new ExtendedHSearchIndexExtractor(
myJpaStorageSettings, myFhirContext, activeSearchParams, mySearchParamExtractor);
ExtendedHSearchIndexData indexData = extractor.extract(new SearchParameter(), searchParams);
ExtendedHSearchIndexData indexData = extractor.extract(new SearchParameter(), new ResourceTable(), searchParams);
// validate
Set<DateSearchIndexData> dIndexData = indexData.getDateIndexData().get("Date");
assertThat(dIndexData).hasSize(0);
Set<QuantitySearchIndexData> qIndexData = indexData.getQuantityIndexData().get("Quantity");
assertThat(qIndexData).hasSize(0);
}
@Override

View File

@ -81,7 +81,7 @@ public class CompositeInterceptorBroadcaster {
}
if (theRequestDetails != null && theRequestDetails.getInterceptorBroadcaster() != null && retVal) {
IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster();
interceptorBroadcaster.callHooks(thePointcut, theParams);
retVal = interceptorBroadcaster.callHooks(thePointcut, theParams);
}
return retVal;
}

View File

@ -0,0 +1,161 @@
package ca.uhn.fhir.rest.server.util;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CompositeInterceptorBroadcasterTest {
@Mock
private IInterceptorBroadcaster myModuleBroadcasterMock;
@Mock
private IInterceptorBroadcaster myReqDetailsBroadcasterMock;
@Mock
private Pointcut myPointcutMock;
@Mock
private HookParams myHookParamsMock;
@Mock
private RequestDetails myRequestDetailsMock;
@Test
void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_RequestDetailsBroadcasterReturnsTrue_ThenReturnsTrue() {
when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock);
when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true);
when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true);
boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock,
myPointcutMock, myHookParamsMock);
assertThat(retVal).isTrue();
verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
}
@Test
void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_RequestDetailsBroadcasterReturnsFalse_ThenReturnsFalse() {
when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock);
when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true);
when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false);
boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock,
myPointcutMock, myHookParamsMock);
assertThat(retVal).isFalse();
verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
}
@Test
void doCallHooks_WhenModuleBroadcasterReturnsFalse_ThenSkipsBroadcasterInRequestDetails_And_ReturnsFalse() {
when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock);
when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false);
boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock,
myPointcutMock, myHookParamsMock);
assertThat(retVal).isFalse();
verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
verify(myReqDetailsBroadcasterMock, never()).callHooks(myPointcutMock, myHookParamsMock);
}
@Test
void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_NullRequestDetailsBroadcaster_ThenReturnsTrue() {
when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true);
when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(null);
boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, myPointcutMock,
myHookParamsMock);
assertThat(retVal).isTrue();
verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
}
@Test
void doCallHooks_WhenModuleBroadcasterReturnsFalse_And_NullRequestDetailsBroadcaster_ThenReturnsFalse() {
when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false);
when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(null);
boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, myPointcutMock,
myHookParamsMock);
assertThat(retVal).isFalse();
verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
}
@Test
void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_NullRequestDetails_ThenReturnsTrue() {
when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true);
boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, null, myPointcutMock, myHookParamsMock);
assertThat(retVal).isTrue();
verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
}
@Test
void doCallHooks_WhenModuleBroadcasterReturnsFalse_And_NullRequestDetails_ThenReturnsFalse() {
when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false);
boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, null, myPointcutMock, myHookParamsMock);
assertThat(retVal).isFalse();
verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
}
@Test
void doCallHooks_WhenNullModuleBroadcaster_And_NullRequestDetails_ThenReturnsTrue() {
boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(null, null, myPointcutMock, myHookParamsMock);
assertThat(retVal).isTrue();
}
@Test
void doCallHooks_WhenNullModuleBroadcaster_And_RequestDetailsBroadcasterReturnsTrue_ThenReturnsTrue() {
when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock);
when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true);
boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(null, myRequestDetailsMock, myPointcutMock, myHookParamsMock);
assertThat(retVal).isTrue();
verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
}
@Test
void doCallHooks_WhenNullModuleBroadcaster_And_RequestDetailsBroadcasterReturnsFalse_ThenReturnsFalse() {
when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock);
when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false);
boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(null, myRequestDetailsMock, myPointcutMock, myHookParamsMock);
assertThat(retVal).isFalse();
verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock);
}
}

View File

@ -92,7 +92,7 @@ public class ReindexV1Config {
"Load IDs of resources to reindex",
ResourceIdListWorkChunkJson.class,
myReindexLoadIdsStep)
.addLastStep("reindex-start", "Start the resource reindex", reindexStepV1())
.addLastStep("reindex", "Start the resource reindex", reindexStepV1())
.build();
}

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
import ca.uhn.fhir.util.BundleBuilder;
import com.google.common.collect.HashMultimap;
@ -66,18 +67,42 @@ public class DaoTestDataBuilder implements ITestDataBuilder.WithSupport, ITestDa
}
//noinspection rawtypes
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
// manipulate the transaction details to provide a fake transaction date
TransactionDetails details = null;
if (theResource.getMeta() != null && theResource.getMeta().getLastUpdated() != null) {
details = new TransactionDetails(theResource.getMeta().getLastUpdated());
} else {
details = new TransactionDetails();
}
//noinspection unchecked
IIdType id = dao.create(theResource, mySrd).getId().toUnqualifiedVersionless();
IIdType id = dao.create(theResource, null, true, mySrd, details)
.getId().toUnqualifiedVersionless();
myIds.put(theResource.fhirType(), id);
return id;
}
@Override
public IIdType doUpdateResource(IBaseResource theResource) {
// manipulate the transaction details to provdie a fake transaction date
TransactionDetails details = null;
if (theResource.getMeta() != null && theResource.getMeta().getLastUpdated() != null) {
details = new TransactionDetails(theResource.getMeta().getLastUpdated());
} else {
details = new TransactionDetails();
}
//noinspection rawtypes
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
//noinspection unchecked
IIdType id = dao.update(theResource, mySrd).getId().toUnqualifiedVersionless();
IIdType id = dao.update(theResource,
null,
true,
false,
mySrd,
details).getId().toUnqualifiedVersionless();
myIds.put(theResource.fhirType(), id);
return id;
}

View File

@ -23,6 +23,7 @@ import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.Device;
import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.DocumentReference;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Extension;
@ -43,6 +44,7 @@ import org.hl7.fhir.r4.model.PrimitiveType;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.QuestionnaireResponse;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Specimen;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Type;
import org.hl7.fhir.r4.model.codesystems.DataAbsentReason;
@ -55,6 +57,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.annotation.Nonnull;
import org.testcontainers.shaded.com.trilead.ssh2.packets.PacketDisconnect;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
@ -261,7 +265,81 @@ public class JsonParserR4Test extends BaseTest {
idx = encoded.indexOf("\"Medication\"", idx + 1);
assertEquals(-1, idx);
}
@Test
public void testDuplicateContainedResourcesAcrossABundleAreReplicated() {
Bundle b = new Bundle();
Specimen specimen = new Specimen();
Practitioner practitioner = new Practitioner();
DiagnosticReport report = new DiagnosticReport();
report.addSpecimen(new Reference(specimen));
b.addEntry().setResource(report).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("/DiagnosticReport");
Observation obs = new Observation();
obs.addPerformer(new Reference(practitioner));
obs.setSpecimen(new Reference(specimen));
b.addEntry().setResource(obs).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("/Observation");
String encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(b);
//Then: Diag should contain one local contained specimen
assertThat(encoded).contains("[{\"resource\":{\"resourceType\":\"DiagnosticReport\",\"contained\":[{\"resourceType\":\"Specimen\",\"id\":\"1\"}]");
//Then: Obs should contain one local contained specimen, and one local contained pract
assertThat(encoded).contains("\"resource\":{\"resourceType\":\"Observation\",\"contained\":[{\"resourceType\":\"Specimen\",\"id\":\"1\"},{\"resourceType\":\"Practitioner\",\"id\":\"2\"}]");
assertThat(encoded).contains("\"performer\":[{\"reference\":\"#2\"}],\"specimen\":{\"reference\":\"#1\"}");
//Also, reverting the operation should work too!
Bundle bundle = ourCtx.newJsonParser().parseResource(Bundle.class, encoded);
IBaseResource resource1 = ((DiagnosticReport) bundle.getEntry().get(0).getResource()).getSpecimenFirstRep().getResource();
IBaseResource resource = ((Observation) bundle.getEntry().get(1).getResource()).getSpecimen().getResource();
assertThat(resource1.getIdElement().getIdPart()).isEqualTo(resource.getIdElement().getIdPart());
assertThat(resource1).isNotSameAs(resource);
}
@Test
public void testAutoAssignedContainedCollisionOrderDependent() {
{
Specimen specimen = new Specimen();
Practitioner practitioner = new Practitioner();
DiagnosticReport report = new DiagnosticReport();
report.addSpecimen(new Reference(specimen));
Observation obs = new Observation();
//When: The practitioner (which is parsed first, has an assigned id that will collide with auto-assigned
practitioner.setId("#1");
obs.addPerformer(new Reference(practitioner));
obs.setSpecimen(new Reference(specimen));
String encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(obs);
assertThat(encoded).contains("\"contained\":[{\"resourceType\":\"Practitioner\",\"id\":\"1\"},{\"resourceType\":\"Specimen\",\"id\":\"2\"}]");
assertThat(encoded).contains("\"performer\":[{\"reference\":\"#1\"}]");
assertThat(encoded).contains("\"specimen\":{\"reference\":\"#2\"}}");
ourLog.info(encoded);
}
{
Specimen specimen = new Specimen();
Practitioner practitioner = new Practitioner();
DiagnosticReport report = new DiagnosticReport();
report.addSpecimen(new Reference(specimen));
Observation obs = new Observation();
//When: The specimen (which is parsed second, has an assigned id that will collide with auto-assigned practitioner
specimen.setId("#1");
obs.addPerformer(new Reference(practitioner));
obs.setSpecimen(new Reference(specimen));
String encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(obs);
assertThat(encoded).contains("\"contained\":[{\"resourceType\":\"Specimen\",\"id\":\"1\"},{\"resourceType\":\"Practitioner\",\"id\":\"2\"}]");
assertThat(encoded).contains("\"performer\":[{\"reference\":\"#2\"}]");
assertThat(encoded).contains("\"specimen\":{\"reference\":\"#1\"}}");
ourLog.info(encoded);
}
}
@Test

View File

@ -6,7 +6,10 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.annotation.Block;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.JsonParser;
import com.google.common.collect.Lists;
import org.apache.jena.base.Sys;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseReference;
@ -47,6 +50,8 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.shaded.com.fasterxml.jackson.core.JsonProcessingException;
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@ -1545,23 +1550,29 @@ public class FhirTerserR4Test {
@Test
void copyingAndParsingCreatesDuplicateContainedResources() {
var input = new Library();
var library = new Library();
var params = new Parameters();
var id = "#expansion-parameters-ecr";
params.setId(id);
params.addParameter("system-version", new StringType("test2"));
var paramsExt = new Extension();
paramsExt.setUrl("test").setValue(new Reference(id));
input.addContained(params);
input.addExtension(paramsExt);
library.addContained(params);
library.addExtension(paramsExt);
final var parser = FhirContext.forR4Cached().newJsonParser();
var stringified = parser.encodeResourceToString(input);
var stringified = parser.encodeResourceToString(library);
var parsed = parser.parseResource(stringified);
var copy = ((Library) parsed).copy();
assertEquals(1, copy.getContained().size());
var stringifiedCopy = parser.encodeResourceToString(copy);
var parsedCopy = parser.parseResource(stringifiedCopy);
assertEquals(1, ((Library) parsedCopy).getContained().size());
String stringifiedCopy = FhirContext.forR4Cached().newJsonParser().encodeResourceToString(copy);
Library parsedCopy = (Library) parser.parseResource(stringifiedCopy);
assertEquals(1, parsedCopy.getContained().size());
}
/**

View File

@ -209,6 +209,12 @@ public interface ITestDataBuilder {
return t -> ((IBaseResource)t).getMeta().setLastUpdated(theLastUpdated);
}
/**
* Sets a _lastUpdated value.
*
* This value will also be used to control the transaction time, which is what determines
* what the Updated date is.
*/
default ICreationArgument withLastUpdated(String theIsoDate) {
return t -> ((IBaseResource)t).getMeta().setLastUpdated(new InstantType(theIsoDate).getValue());
}

View File

@ -0,0 +1,123 @@
/*-
* #%L
* HAPI FHIR Test Utilities
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.test.utilities.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.util.ClasspathUtil;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IDomainResource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public interface IValidationProviders {
String CODE_SYSTEM = "http://code.system/url";
String CODE_SYSTEM_VERSION = "1.0.0";
String CODE_SYSTEM_NAME = "Test Code System";
String CODE = "CODE";
String VALUE_SET_URL = "http://value.set/url";
String DISPLAY = "Explanation for code TestCode.";
String LANGUAGE = "en";
String ERROR_MESSAGE = "This is an error message";
interface IMyValidationProvider extends IResourceProvider {
void addException(String theOperation, String theUrl, String theCode, Exception theException);
<P extends IBaseParameters> void addTerminologyResponse(String theOperation, String theUrl, String theCode, P theReturnParams);
IBaseParameters addTerminologyResponse(String theOperation, String theUrl, String theCode, FhirContext theFhirContext, String theTerminologyResponseFile);
}
abstract class MyValidationProvider<T extends IDomainResource> implements IMyValidationProvider {
private final Map<String, Exception> myExceptionMap = new HashMap<>();
private boolean myShouldThrowExceptionForResourceNotFound = true;
private final Map<String, IBaseParameters> myTerminologyResponseMap = new HashMap<>();
private final Map<String, T> myTerminologyResourceMap = new HashMap<>();
static String getInputKey(String theOperation, String theUrl, String theCode) {
return theOperation + "-" + theUrl + "#" + theCode;
}
public void setShouldThrowExceptionForResourceNotFound(boolean theShouldThrowExceptionForResourceNotFound) {
myShouldThrowExceptionForResourceNotFound = theShouldThrowExceptionForResourceNotFound;
}
public void addException(String theOperation, String theUrl, String theCode, Exception theException) {
String inputKey = getInputKey(theOperation, theUrl, theCode);
myExceptionMap.put(inputKey, theException);
}
abstract Class<? extends IBaseParameters> getParameterType();
@Override
public <P extends IBaseParameters> void addTerminologyResponse(String theOperation, String theUrl, String theCode, P theReturnParams) {
myTerminologyResponseMap.put(getInputKey(theOperation, theUrl, theCode), theReturnParams);
}
public IBaseParameters addTerminologyResponse(String theOperation, String theUrl, String theCode, FhirContext theFhirContext, String theTerminologyResponseFile) {
IBaseParameters responseParams = ClasspathUtil.loadResource(theFhirContext, getParameterType(), theTerminologyResponseFile);
addTerminologyResponse(theOperation, theUrl, theCode, responseParams);
return responseParams;
}
protected void addTerminologyResource(String theUrl, T theResource) {
myTerminologyResourceMap.put(theUrl, theResource);
}
public abstract T addTerminologyResource(String theUrl);
protected IBaseParameters getTerminologyResponse(String theOperation, String theUrl, String theCode) throws Exception {
String inputKey = getInputKey(theOperation, theUrl, theCode);
if (myExceptionMap.containsKey(inputKey)) {
throw myExceptionMap.get(inputKey);
}
IBaseParameters params = myTerminologyResponseMap.get(inputKey);
if (params == null) {
throw new IllegalStateException("Test setup incomplete. Missing return params for " + inputKey);
}
return params;
}
protected T getTerminologyResource(UriParam theUrlParam) {
if (theUrlParam.isEmpty()) {
throw new IllegalStateException("CodeSystem url should not be null.");
}
String urlValue = theUrlParam.getValue();
if (!myTerminologyResourceMap.containsKey(urlValue) && myShouldThrowExceptionForResourceNotFound) {
throw new IllegalStateException("Test setup incomplete. CodeSystem not found " + urlValue);
}
return myTerminologyResourceMap.get(urlValue);
}
@Search
public List<T> find(@RequiredParam(name = "url") UriParam theUrlParam) {
T resource = getTerminologyResource(theUrlParam);
return resource != null ? List.of(resource) : List.of();
}
}
interface IMyLookupCodeProvider extends IResourceProvider {
void setLookupCodeResult(IValidationSupport.LookupCodeResult theLookupCodeResult);
}
}

View File

@ -0,0 +1,137 @@
/*-
* #%L
* HAPI FHIR Test Utilities
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.test.utilities.validation;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.dstu3.model.BooleanType;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.CodeType;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Parameters;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
public interface IValidationProvidersDstu3 {
@SuppressWarnings("unused")
class MyCodeSystemProviderDstu3 extends IValidationProviders.MyValidationProvider<CodeSystem> {
@Operation(name = "$validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = StringType.class),
@OperationParam(name = "display", type = StringType.class)
})
public IBaseParameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) IdType theId,
@OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl,
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay
) throws Exception {
String url = theCodeSystemUrl != null ? theCodeSystemUrl.getValue() : null;
String code = theCode != null ? theCode.getValue() : null;
return getTerminologyResponse("$validate-code", url, code);
}
@Operation(name = "$lookup", idempotent = true, returnParameters= {
@OperationParam(name = "name", type = StringType.class, min = 1),
@OperationParam(name = "version", type = StringType.class),
@OperationParam(name = "display", type = StringType.class, min = 1),
@OperationParam(name = "abstract", type = BooleanType.class, min = 1),
@OperationParam(name = "property", type = StringType.class, min = 0, max = OperationParam.MAX_UNLIMITED)
})
public IBaseParameters lookup(
HttpServletRequest theServletRequest,
@OperationParam(name = "code", max = 1) CodeType theCode,
@OperationParam(name = "system",max = 1) UriType theSystem,
@OperationParam(name = "coding", max = 1) Coding theCoding,
@OperationParam(name = "version", max = 1) StringType theVersion,
@OperationParam(name = "displayLanguage", max = 1) CodeType theDisplayLanguage,
@OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List<CodeType> thePropertyNames,
RequestDetails theRequestDetails
) throws Exception {
String url = theSystem != null ? theSystem.getValue() : null;
String code = theCode != null ? theCode.getValue() : null;
return getTerminologyResponse("$lookup", url, code);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
@Override
Class<Parameters> getParameterType() {
return Parameters.class;
}
@Override
public CodeSystem addTerminologyResource(String theUrl) {
CodeSystem codeSystem = new CodeSystem();
codeSystem.setId(theUrl.substring(0, theUrl.lastIndexOf("/")));
codeSystem.setUrl(theUrl);
addTerminologyResource(theUrl, codeSystem);
return codeSystem;
}
}
@SuppressWarnings("unused")
class MyValueSetProviderDstu3 extends IValidationProviders.MyValidationProvider<ValueSet> {
@Operation(name = "$validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = StringType.class),
@OperationParam(name = "display", type = StringType.class)
})
public IBaseParameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) IdType theId,
@OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl,
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
@OperationParam(name = "system", min = 0, max = 1) UriType theSystem,
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay,
@OperationParam(name = "valueSet") ValueSet theValueSet
) throws Exception {
String url = theValueSetUrl != null ? theValueSetUrl.getValue() : null;
String code = theCode != null ? theCode.getValue() : null;
return getTerminologyResponse("$validate-code", url, code);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return ValueSet.class;
}
@Override
Class<Parameters> getParameterType() {
return Parameters.class;
}
@Override
public ValueSet addTerminologyResource(String theUrl) {
ValueSet valueSet = new ValueSet();
valueSet.setId(theUrl.substring(0, theUrl.lastIndexOf("/")));
valueSet.setUrl(theUrl);
addTerminologyResource(theUrl, valueSet);
return valueSet;
}
}
}

View File

@ -1,12 +1,29 @@
package org.hl7.fhir.r4.validation;
/*-
* #%L
* HAPI FHIR Test Utilities
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.test.utilities.validation;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.common.hapi.validation.IValidationProviders;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.BooleanType;
@ -21,38 +38,29 @@ import org.hl7.fhir.r4.model.ValueSet;
import java.util.List;
public interface IValidateCodeProvidersR4 {
public interface IValidationProvidersR4 {
@SuppressWarnings("unused")
class MyCodeSystemProviderR4 implements IValidationProviders.IMyCodeSystemProvider {
private UriType mySystemUrl;
private CodeType myCode;
private StringType myDisplay;
private Exception myException;
private Parameters myReturnParams;
class MyCodeSystemProviderR4 extends IValidationProviders.MyValidationProvider<CodeSystem> {
@Operation(name = "validate-code", idempotent = true, returnParameters = {
@Operation(name = "$validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = StringType.class),
@OperationParam(name = "display", type = StringType.class)
})
public Parameters validateCode(
public IBaseParameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) IdType theId,
@OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl,
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay
) throws Exception {
mySystemUrl = theCodeSystemUrl;
myCode = theCode;
myDisplay = theDisplay;
if (myException != null) {
throw myException;
}
return myReturnParams;
String url = theCodeSystemUrl != null ? theCodeSystemUrl.getValue() : null;
String code = theCode != null ? theCode.getValue() : null;
return getTerminologyResponse("$validate-code", url, code);
}
@Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= {
@Operation(name = "$lookup", idempotent = true, returnParameters= {
@OperationParam(name = "name", type = StringType.class, min = 1),
@OperationParam(name = "version", type = StringType.class),
@OperationParam(name = "display", type = StringType.class, min = 1),
@ -69,54 +77,39 @@ public interface IValidateCodeProvidersR4 {
@OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List<CodeType> thePropertyNames,
RequestDetails theRequestDetails
) throws Exception {
mySystemUrl = theSystem;
myCode = theCode;
if (myException != null) {
throw myException;
}
return myReturnParams;
String url = theSystem != null ? theSystem.getValue() : null;
String code = theCode != null ? theCode.getValue() : null;
return getTerminologyResponse("$lookup", url, code);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
public void setException(Exception theException) {
myException = theException;
}
@Override
public void setReturnParams(IBaseParameters theParameters) {
myReturnParams = (Parameters) theParameters;
Class<Parameters> getParameterType() {
return Parameters.class;
}
@Override
public String getCode() {
return myCode != null ? myCode.getValueAsString() : null;
}
@Override
public String getSystem() {
return mySystemUrl != null ? mySystemUrl.getValueAsString() : null;
}
public String getDisplay() {
return myDisplay != null ? myDisplay.getValue() : null;
public CodeSystem addTerminologyResource(String theUrl) {
CodeSystem codeSystem = new CodeSystem();
codeSystem.setId(theUrl.substring(0, theUrl.lastIndexOf("/")));
codeSystem.setUrl(theUrl);
addTerminologyResource(theUrl, codeSystem);
return codeSystem;
}
}
@SuppressWarnings("unused")
class MyValueSetProviderR4 implements IValidationProviders.IMyValueSetProvider {
private Exception myException;
private Parameters myReturnParams;
private UriType mySystemUrl;
private UriType myValueSetUrl;
private CodeType myCode;
private StringType myDisplay;
class MyValueSetProviderR4 extends IValidationProviders.MyValidationProvider<ValueSet> {
@Operation(name = "validate-code", idempotent = true, returnParameters = {
@Operation(name = "$validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = StringType.class),
@OperationParam(name = "display", type = StringType.class)
})
public Parameters validateCode(
public IBaseParameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) IdType theId,
@OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl,
@ -125,41 +118,25 @@ public interface IValidateCodeProvidersR4 {
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay,
@OperationParam(name = "valueSet") ValueSet theValueSet
) throws Exception {
mySystemUrl = theSystem;
myValueSetUrl = theValueSetUrl;
myCode = theCode;
myDisplay = theDisplay;
if (myException != null) {
throw myException;
}
return myReturnParams;
String url = theValueSetUrl != null ? theValueSetUrl.getValue() : null;
String code = theCode != null ? theCode.getValue() : null;
return getTerminologyResponse("$validate-code", url, code);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return ValueSet.class;
}
public void setException(Exception theException) {
myException = theException;
@Override
Class<Parameters> getParameterType() {
return Parameters.class;
}
@Override
public void setReturnParams(IBaseParameters theParameters) {
myReturnParams = (Parameters) theParameters;
}
@Override
public String getCode() {
return myCode != null ? myCode.getValueAsString() : null;
}
@Override
public String getSystem() {
return mySystemUrl != null ? mySystemUrl.getValueAsString() : null;
}
@Override
public String getValueSet() {
return myValueSetUrl != null ? myValueSetUrl.getValueAsString() : null;
}
public String getDisplay() {
return myDisplay != null ? myDisplay.getValue() : null;
public ValueSet addTerminologyResource(String theUrl) {
ValueSet valueSet = new ValueSet();
valueSet.setId(theUrl.substring(0, theUrl.lastIndexOf("/")));
valueSet.setUrl(theUrl);
addTerminologyResource(theUrl, valueSet);
return valueSet;
}
}
}

View File

@ -190,7 +190,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport {
return new CodeValidationResult()
.setSeverity(IssueSeverity.ERROR)
.setMessage(theMessage)
.setCodeValidationIssues(Collections.singletonList(new CodeValidationIssue(
.setIssues(Collections.singletonList(new CodeValidationIssue(
theMessage,
IssueSeverity.ERROR,
CodeValidationIssueCode.INVALID,

View File

@ -28,7 +28,6 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r5.model.CanonicalType;
import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.Enumerations;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import java.util.ArrayList;
import java.util.Collections;
@ -258,7 +257,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
theValidationSupportContext, theValueSet, theCodeSystemUrlAndVersion, theCode);
} catch (ExpansionCouldNotBeCompletedInternallyException e) {
CodeValidationResult codeValidationResult = new CodeValidationResult();
codeValidationResult.setSeverityCode("error");
codeValidationResult.setSeverity(IssueSeverity.ERROR);
String msg = "Failed to expand ValueSet '" + vsUrl + "' (in-memory). Could not validate code "
+ theCodeSystemUrlAndVersion + "#" + theCode;
@ -267,7 +266,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
}
codeValidationResult.setMessage(msg);
codeValidationResult.addCodeValidationIssue(e.getCodeValidationIssue());
codeValidationResult.addIssue(e.getCodeValidationIssue());
return codeValidationResult;
}
@ -551,18 +550,18 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
if (valueSetResult != null) {
codeValidationResult = valueSetResult;
} else {
ValidationMessage.IssueSeverity severity;
IValidationSupport.IssueSeverity severity;
String message;
CodeValidationIssueCode issueCode = CodeValidationIssueCode.CODE_INVALID;
CodeValidationIssueCoding issueCoding = CodeValidationIssueCoding.INVALID_CODE;
if ("fragment".equals(codeSystemResourceContentMode)) {
severity = ValidationMessage.IssueSeverity.WARNING;
severity = IValidationSupport.IssueSeverity.WARNING;
message = "Unknown code in fragment CodeSystem '"
+ getFormattedCodeSystemAndCodeForMessage(
theCodeSystemUrlAndVersionToValidate, theCodeToValidate)
+ "'";
} else {
severity = ValidationMessage.IssueSeverity.ERROR;
severity = IValidationSupport.IssueSeverity.ERROR;
message = "Unknown code '"
+ getFormattedCodeSystemAndCodeForMessage(
theCodeSystemUrlAndVersionToValidate, theCodeToValidate)
@ -574,10 +573,9 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
}
codeValidationResult = new CodeValidationResult()
.setSeverityCode(severity.toCode())
.setSeverity(severity)
.setMessage(message)
.addCodeValidationIssue(new CodeValidationIssue(
message, getIssueSeverityFromCodeValidationIssue(severity), issueCode, issueCoding));
.addIssue(new CodeValidationIssue(message, severity, issueCode, issueCoding));
}
return codeValidationResult;
@ -589,19 +587,6 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
+ theCodeToValidate;
}
private IValidationSupport.IssueSeverity getIssueSeverityFromCodeValidationIssue(
ValidationMessage.IssueSeverity theSeverity) {
switch (theSeverity) {
case ERROR:
return IValidationSupport.IssueSeverity.ERROR;
case WARNING:
return IValidationSupport.IssueSeverity.WARNING;
case INFORMATION:
return IValidationSupport.IssueSeverity.INFORMATION;
}
return null;
}
private CodeValidationResult findCodeInExpansion(
String theCodeToValidate,
String theDisplayToValidate,
@ -1123,8 +1108,8 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
new CodeValidationIssue(
theMessage,
IssueSeverity.ERROR,
CodeValidationIssueCode.OTHER,
CodeValidationIssueCoding.OTHER));
CodeValidationIssueCode.INVALID,
CodeValidationIssueCoding.VS_INVALID));
}
for (org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent next :
subExpansion.getExpansion().getContains()) {
@ -1376,7 +1361,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
.setCodeSystemVersion(theCodeSystemVersion)
.setDisplay(theExpectedDisplay);
if (issueSeverity != null) {
codeValidationResult.setCodeValidationIssues(Collections.singletonList(new CodeValidationIssue(
codeValidationResult.setIssues(Collections.singletonList(new CodeValidationIssue(
message,
theIssueSeverityForCodeDisplayMismatch,
CodeValidationIssueCode.INVALID,

View File

@ -28,6 +28,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Base;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Parameters;
@ -631,7 +632,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
return new CodeValidationResult()
.setSeverity(severity)
.setMessage(theMessage)
.addCodeValidationIssue(new CodeValidationIssue(
.addIssue(new CodeValidationIssue(
theMessage, severity, theIssueCode, CodeValidationIssueCoding.INVALID_CODE));
}
@ -680,13 +681,13 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
createCodeValidationIssues(
(IBaseOperationOutcome) issuesValue.get(),
fhirContext.getVersion().getVersion())
.ifPresent(i -> i.forEach(result::addCodeValidationIssue));
.ifPresent(i -> i.forEach(result::addIssue));
} else {
// create a validation issue out of the message
// this is a workaround to overcome an issue in the FHIR Validator library
// where ValueSet bindings are only reading issues but not messages
// @see https://github.com/hapifhir/org.hl7.fhir.core/issues/1766
result.addCodeValidationIssue(createCodeValidationIssue(result.getMessage()));
result.addIssue(createCodeValidationIssue(result.getMessage()));
}
return result;
}
@ -717,23 +718,42 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
private static Collection<CodeValidationIssue> createCodeValidationIssuesR4(OperationOutcome theOperationOutcome) {
return theOperationOutcome.getIssue().stream()
.map(issueComponent ->
createCodeValidationIssue(issueComponent.getDetails().getText()))
.map(issueComponent -> {
String diagnostics = issueComponent.getDiagnostics();
IssueSeverity issueSeverity =
IssueSeverity.fromCode(issueComponent.getSeverity().toCode());
String issueTypeCode = issueComponent.getCode().toCode();
CodeableConcept details = issueComponent.getDetails();
CodeValidationIssue issue = new CodeValidationIssue(diagnostics, issueSeverity, issueTypeCode);
CodeValidationIssueDetails issueDetails = new CodeValidationIssueDetails(details.getText());
details.getCoding().forEach(coding -> issueDetails.addCoding(coding.getSystem(), coding.getCode()));
issue.setDetails(issueDetails);
return issue;
})
.collect(Collectors.toList());
}
private static Collection<CodeValidationIssue> createCodeValidationIssuesDstu3(
org.hl7.fhir.dstu3.model.OperationOutcome theOperationOutcome) {
return theOperationOutcome.getIssue().stream()
.map(issueComponent ->
createCodeValidationIssue(issueComponent.getDetails().getText()))
.map(issueComponent -> {
String diagnostics = issueComponent.getDiagnostics();
IssueSeverity issueSeverity =
IssueSeverity.fromCode(issueComponent.getSeverity().toCode());
String issueTypeCode = issueComponent.getCode().toCode();
org.hl7.fhir.dstu3.model.CodeableConcept details = issueComponent.getDetails();
CodeValidationIssue issue = new CodeValidationIssue(diagnostics, issueSeverity, issueTypeCode);
CodeValidationIssueDetails issueDetails = new CodeValidationIssueDetails(details.getText());
details.getCoding().forEach(coding -> issueDetails.addCoding(coding.getSystem(), coding.getCode()));
issue.setDetails(issueDetails);
return issue;
})
.collect(Collectors.toList());
}
private static CodeValidationIssue createCodeValidationIssue(String theMessage) {
return new CodeValidationIssue(
theMessage,
// assume issue type is OperationOutcome.IssueType#CODEINVALID as it is the only match
IssueSeverity.ERROR,
CodeValidationIssueCode.INVALID,
CodeValidationIssueCoding.INVALID_CODE);

View File

@ -87,7 +87,7 @@ public class UnknownCodeSystemWarningValidationSupport extends BaseValidationSup
result.setSeverity(null);
result.setMessage(null);
} else {
result.addCodeValidationIssue(new CodeValidationIssue(
result.addIssue(new CodeValidationIssue(
theMessage,
myNonExistentCodeSystemSeverity,
CodeValidationIssueCode.NOT_FOUND,

View File

@ -11,6 +11,17 @@ public final class ValidationSupportUtils {
private ValidationSupportUtils() {}
/**
* This method extracts a code system that can be (potentially) associated with a code when
* performing validation against a ValueSet. This method was created for internal purposes.
* Please use this method with care because it will only cover some
* use-cases (e.g. standard bindings) while for others it may not return correct results or return null.
* An incorrect result could be considered if the resource declares a code with a system, and you're calling
* this method to check a binding against a ValueSet that has nothing to do with that system.
* @param theValueSet the valueSet
* @param theCode the code
* @return the system which can be associated with the code
*/
public static String extractCodeSystemForCode(IBaseResource theValueSet, String theCode) {
if (theValueSet instanceof org.hl7.fhir.dstu3.model.ValueSet) {
return extractCodeSystemForCodeDSTU3((org.hl7.fhir.dstu3.model.ValueSet) theValueSet, theCode);

View File

@ -62,6 +62,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssueCoding.INVALID_DISPLAY;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -296,7 +297,7 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo
theResult.getCodeSystemVersion(),
conceptDefinitionComponent,
display,
getIssuesForCodeValidation(theResult.getCodeValidationIssues()));
getIssuesForCodeValidation(theResult.getIssues()));
}
if (retVal == null) {
@ -307,73 +308,36 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo
}
private List<OperationOutcome.OperationOutcomeIssueComponent> getIssuesForCodeValidation(
List<IValidationSupport.CodeValidationIssue> codeValidationIssues) {
List<OperationOutcome.OperationOutcomeIssueComponent> issues = new ArrayList<>();
List<IValidationSupport.CodeValidationIssue> theIssues) {
List<OperationOutcome.OperationOutcomeIssueComponent> issueComponents = new ArrayList<>();
for (IValidationSupport.CodeValidationIssue codeValidationIssue : codeValidationIssues) {
for (IValidationSupport.CodeValidationIssue issue : theIssues) {
OperationOutcome.IssueSeverity severity =
OperationOutcome.IssueSeverity.fromCode(issue.getSeverity().getCode());
OperationOutcome.IssueType issueType =
OperationOutcome.IssueType.fromCode(issue.getType().getCode());
String diagnostics = issue.getDiagnostics();
CodeableConcept codeableConcept = new CodeableConcept().setText(codeValidationIssue.getMessage());
codeableConcept.addCoding(
"http://hl7.org/fhir/tools/CodeSystem/tx-issue-type",
getIssueCodingFromCodeValidationIssue(codeValidationIssue),
null);
IValidationSupport.CodeValidationIssueDetails details = issue.getDetails();
CodeableConcept codeableConcept = new CodeableConcept().setText(details.getText());
details.getCodings().forEach(detailCoding -> codeableConcept
.addCoding()
.setSystem(detailCoding.getSystem())
.setCode(detailCoding.getCode()));
OperationOutcome.OperationOutcomeIssueComponent issue =
OperationOutcome.OperationOutcomeIssueComponent issueComponent =
new OperationOutcome.OperationOutcomeIssueComponent()
.setSeverity(getIssueSeverityFromCodeValidationIssue(codeValidationIssue))
.setCode(getIssueTypeFromCodeValidationIssue(codeValidationIssue))
.setDetails(codeableConcept);
issue.getDetails().setText(codeValidationIssue.getMessage());
issue.addExtension()
.setSeverity(severity)
.setCode(issueType)
.setDetails(codeableConcept)
.setDiagnostics(diagnostics);
issueComponent
.addExtension()
.setUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id")
.setValue(new StringType("Terminology_PassThrough_TX_Message"));
issues.add(issue);
issueComponents.add(issueComponent);
}
return issues;
}
private String getIssueCodingFromCodeValidationIssue(IValidationSupport.CodeValidationIssue codeValidationIssue) {
switch (codeValidationIssue.getCoding()) {
case VS_INVALID:
return "vs-invalid";
case NOT_FOUND:
return "not-found";
case NOT_IN_VS:
return "not-in-vs";
case INVALID_CODE:
return "invalid-code";
case INVALID_DISPLAY:
return "invalid-display";
}
return null;
}
private OperationOutcome.IssueType getIssueTypeFromCodeValidationIssue(
IValidationSupport.CodeValidationIssue codeValidationIssue) {
switch (codeValidationIssue.getCode()) {
case NOT_FOUND:
return OperationOutcome.IssueType.NOTFOUND;
case CODE_INVALID:
return OperationOutcome.IssueType.CODEINVALID;
case INVALID:
return OperationOutcome.IssueType.INVALID;
}
return null;
}
private OperationOutcome.IssueSeverity getIssueSeverityFromCodeValidationIssue(
IValidationSupport.CodeValidationIssue codeValidationIssue) {
switch (codeValidationIssue.getSeverity()) {
case FATAL:
return OperationOutcome.IssueSeverity.FATAL;
case ERROR:
return OperationOutcome.IssueSeverity.ERROR;
case WARNING:
return OperationOutcome.IssueSeverity.WARNING;
case INFORMATION:
return OperationOutcome.IssueSeverity.INFORMATION;
}
return null;
return issueComponents;
}
@Override
@ -851,25 +815,22 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo
.getRootValidationSupport()
.validateCodeInValueSet(
myValidationSupportContext, theValidationOptions, theSystem, theCode, theDisplay, theValueSet);
if (result != null) {
if (result != null && theSystem != null) {
/* We got a value set result, which could be successful, or could contain errors/warnings. The code
might also be invalid in the code system, so we will check that as well and add those issues
to our result.
*/
IValidationSupport.CodeValidationResult codeSystemResult =
validateCodeInCodeSystem(theValidationOptions, theSystem, theCode, theDisplay);
final boolean valueSetResultContainsInvalidDisplay = result.getCodeValidationIssues().stream()
.anyMatch(codeValidationIssue -> codeValidationIssue.getCoding()
== IValidationSupport.CodeValidationIssueCoding.INVALID_DISPLAY);
final boolean valueSetResultContainsInvalidDisplay = result.getIssues().stream()
.anyMatch(VersionSpecificWorkerContextWrapper::hasInvalidDisplayDetailCode);
if (codeSystemResult != null) {
for (IValidationSupport.CodeValidationIssue codeValidationIssue :
codeSystemResult.getCodeValidationIssues()) {
for (IValidationSupport.CodeValidationIssue codeValidationIssue : codeSystemResult.getIssues()) {
/* Value set validation should already have checked the display name. If we get INVALID_DISPLAY
issues from code system validation, they will only repeat what was already caught.
*/
if (codeValidationIssue.getCoding() != IValidationSupport.CodeValidationIssueCoding.INVALID_DISPLAY
|| !valueSetResultContainsInvalidDisplay) {
result.addCodeValidationIssue(codeValidationIssue);
if (!hasInvalidDisplayDetailCode(codeValidationIssue) || !valueSetResultContainsInvalidDisplay) {
result.addIssue(codeValidationIssue);
}
}
}
@ -877,6 +838,10 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo
return result;
}
private static boolean hasInvalidDisplayDetailCode(IValidationSupport.CodeValidationIssue theIssue) {
return theIssue.hasIssueDetailCode(INVALID_DISPLAY.getCode());
}
private IValidationSupport.CodeValidationResult validateCodeInCodeSystem(
ConceptValidationOptions theValidationOptions, String theSystem, String theCode, String theDisplay) {
return myValidationSupportContext

View File

@ -9,6 +9,7 @@ import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult;
import ca.uhn.fhir.context.support.IValidationSupport.StringConceptProperty;
import ca.uhn.fhir.context.support.LookupCodeRequest;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.test.utilities.validation.IValidationProviders;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.junit.jupiter.api.Test;
@ -21,12 +22,12 @@ import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_GROUP;
import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_STRING;
import static java.util.stream.IntStream.range;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE_SYSTEM;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE_SYSTEM_NAME;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE_SYSTEM_VERSION;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.DISPLAY;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.LANGUAGE;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM_NAME;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM_VERSION;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.DISPLAY;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.LANGUAGE;
import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.createConceptProperty;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -189,8 +190,6 @@ public interface ILookupCodeTest {
// verify
assertNotNull(outcome);
assertEquals(theRequest.getCode(), getLookupCodeProvider().getCode());
assertEquals(theRequest.getSystem(), getLookupCodeProvider().getSystem());
assertEquals(theExpectedResult.isFound(), outcome.isFound());
assertEquals(theExpectedResult.getErrorMessage(), outcome.getErrorMessage());
assertEquals(theExpectedResult.getCodeSystemDisplayName(), outcome.getCodeSystemDisplayName());

View File

@ -2,6 +2,7 @@ package org.hl7.fhir.common.hapi.validation;
import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult;
import ca.uhn.fhir.context.support.LookupCodeRequest;
import ca.uhn.fhir.test.utilities.validation.IValidationProviders;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.junit.jupiter.api.Test;

View File

@ -1,17 +1,76 @@
package org.hl7.fhir.common.hapi.validation;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.support.IValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.junit.jupiter.api.Test;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.createCodeValidationIssues;
public interface IRemoteTerminologyValidateCodeTest extends IValidateCodeTest {
default List<IValidationSupport.CodeValidationIssue> getCodeValidationIssues(IBaseOperationOutcome theOperationOutcome) {
// this method should be removed once support for issues is fully implemented across all validator types
Optional<Collection<IValidationSupport.CodeValidationIssue>> issues = RemoteTerminologyServiceValidationSupport.createCodeValidationIssues(theOperationOutcome, getService().getFhirContext().getVersion().getVersion());
return issues.map(theCodeValidationIssues -> theCodeValidationIssues.stream().toList()).orElseGet(List::of);
}
@Test
default void createCodeValidationIssues_withCodeSystemOutcomeForInvalidCode_returnsAsExpected() {
IBaseOperationOutcome outcome = getCodeSystemInvalidCodeOutcome();
FhirVersionEnum versionEnum = getService().getFhirContext().getVersion().getVersion();
Optional<Collection<IValidationSupport.CodeValidationIssue>> issuesOptional = createCodeValidationIssues(outcome, versionEnum);
assertThat(issuesOptional).isPresent();
assertThat(issuesOptional.get()).hasSize(1);
IValidationSupport.CodeValidationIssue issue = issuesOptional.get().iterator().next();
assertThat(issue.getType().getCode()).isEqualTo("code-invalid");
assertThat(issue.getSeverity().getCode()).isEqualTo("error");
assertThat(issue.getDetails().getCodings()).hasSize(1);
IValidationSupport.CodeValidationIssueCoding issueCoding = issue.getDetails().getCodings().get(0);
assertThat(issueCoding.getSystem()).isEqualTo("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type");
assertThat(issueCoding.getCode()).isEqualTo("invalid-code");
assertThat(issue.getDetails().getText()).isEqualTo("Unknown code 'CODE' in the CodeSystem 'http://code.system/url' version '1.0.0'");
assertThat(issue.getDiagnostics()).isNull();
}
@Test
default void createCodeValidationIssues_withValueSetOutcomeForInvalidCode_returnsAsExpected() {
IBaseOperationOutcome outcome = getValueSetInvalidCodeOutcome();
FhirVersionEnum versionEnum = getService().getFhirContext().getVersion().getVersion();
Optional<Collection<IValidationSupport.CodeValidationIssue>> issuesOptional = createCodeValidationIssues(outcome, versionEnum);
assertThat(issuesOptional).isPresent();
assertThat(issuesOptional.get()).hasSize(2);
IValidationSupport.CodeValidationIssue issue = issuesOptional.get().iterator().next();
assertThat(issue.getType().getCode()).isEqualTo("code-invalid");
assertThat(issue.getSeverity().getCode()).isEqualTo("error");
assertThat(issue.getDetails().getCodings()).hasSize(1);
IValidationSupport.CodeValidationIssueCoding issueCoding = issue.getDetails().getCodings().get(0);
assertThat(issueCoding.getSystem()).isEqualTo("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type");
assertThat(issueCoding.getCode()).isEqualTo("not-in-vs");
assertThat(issue.getDetails().getText()).isEqualTo("The provided code 'http://code.system/url#CODE' was not found in the value set 'http://value.set/url%7C1.0.0'");
assertThat(issue.getDiagnostics()).isNull();
}
@Test
default void createCodeValidationIssues_withValueSetOutcomeWithCustomDetailCode_returnsAsExpected() {
IBaseOperationOutcome outcome = getValueSetCustomDetailCodeOutcome();
FhirVersionEnum versionEnum = getService().getFhirContext().getVersion().getVersion();
Optional<Collection<IValidationSupport.CodeValidationIssue>> issuesOptional = createCodeValidationIssues(outcome, versionEnum);
assertThat(issuesOptional).isPresent();
assertThat(issuesOptional.get()).hasSize(1);
IValidationSupport.CodeValidationIssue issue = issuesOptional.get().iterator().next();
assertThat(issue.getType().getCode()).isEqualTo("processing");
assertThat(issue.getSeverity().getCode()).isEqualTo("information");
assertThat(issue.getDetails().getCodings()).hasSize(1);
IValidationSupport.CodeValidationIssueCoding issueCoding = issue.getDetails().getCodings().get(0);
assertThat(issueCoding.getSystem()).isEqualTo("http://example.com/custom-issue-type");
assertThat(issueCoding.getCode()).isEqualTo("valueset-is-draft");
assertThat(issue.getDetails().getText()).isNull();
assertThat(issue.getDiagnostics()).isEqualTo("The ValueSet status is marked as draft.");
}
}

View File

@ -4,6 +4,9 @@ import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.test.utilities.validation.IValidationProviders;
import ca.uhn.fhir.util.ClasspathUtil;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -16,12 +19,13 @@ import java.util.List;
import java.util.stream.Stream;
import static ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity.ERROR;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE_SYSTEM;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE_SYSTEM_VERSION;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.DISPLAY;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.ERROR_MESSAGE;
import static org.hl7.fhir.common.hapi.validation.IValidationProviders.VALUE_SET_URL;
import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_VALIDATE_CODE;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM_VERSION;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.DISPLAY;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.ERROR_MESSAGE;
import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.VALUE_SET_URL;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -31,21 +35,35 @@ import static org.junit.jupiter.api.Assertions.fail;
public interface IValidateCodeTest {
IValidationProviders.IMyCodeSystemProvider getCodeSystemProvider();
IValidationProviders.IMyValueSetProvider getValueSetProvider();
IValidationProviders.IMyValidationProvider getCodeSystemProvider();
IValidationProviders.IMyValidationProvider getValueSetProvider();
IValidationSupport getService();
IBaseParameters createParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource);
String getCodeSystemError();
String getValueSetError();
IBaseOperationOutcome getCodeSystemInvalidCodeOutcome();
IBaseOperationOutcome getValueSetInvalidCodeOutcome();
IBaseOperationOutcome getValueSetCustomDetailCodeOutcome();
default IBaseOperationOutcome getCodeSystemInvalidCodeOutcome(Class<? extends IBaseOperationOutcome> theResourceClass) {
return getOutcome(theResourceClass, "/terminology/OperationOutcome-CodeSystem-invalid-code.json");
}
default IBaseOperationOutcome getValueSetInvalidCodeOutcome(Class<? extends IBaseOperationOutcome> theResourceClass) {
return getOutcome(theResourceClass, "/terminology/OperationOutcome-ValueSet-invalid-code.json");
}
default IBaseOperationOutcome getValueSetCustomDetailCodeOutcome(Class<? extends IBaseOperationOutcome> theResourceClass) {
return getOutcome(theResourceClass, "/terminology/OperationOutcome-ValueSet-custom-issue-detail.json");
}
default IBaseOperationOutcome getOutcome(Class<? extends IBaseOperationOutcome> theResourceClass, String theFile) {
return ClasspathUtil.loadResource(getService().getFhirContext(), theResourceClass, theFile);
}
default void createCodeSystemReturnParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) {
getCodeSystemProvider().setReturnParams(createParameters(theResult, theDisplay, theMessage, theIssuesResource));
getCodeSystemProvider().addTerminologyResponse(OPERATION_VALIDATE_CODE, CODE_SYSTEM, CODE, createParameters(theResult, theDisplay, theMessage, theIssuesResource));
}
default void createValueSetReturnParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) {
getValueSetProvider().setReturnParams(createParameters(theResult, theDisplay, theMessage, theIssuesResource));
getValueSetProvider().addTerminologyResponse(OPERATION_VALIDATE_CODE, VALUE_SET_URL, CODE, createParameters(theResult, theDisplay, theMessage, theIssuesResource));
}
@Test
@ -91,8 +109,8 @@ public interface IValidateCodeTest {
String theValidationMessage,
String theCodeSystem,
String theValueSetUrl) {
getCodeSystemProvider().setException(theException);
getValueSetProvider().setException(theException);
getCodeSystemProvider().addException(OPERATION_VALIDATE_CODE, theCodeSystem, CODE, theException);
getValueSetProvider().addException(OPERATION_VALIDATE_CODE, theValueSetUrl, CODE, theException);
CodeValidationResult outcome = getService().validateCode(null, null, theCodeSystem, CODE, DISPLAY, theValueSetUrl);
verifyErrorResultFromException(outcome, theValidationMessage, theServerMessage);
@ -105,7 +123,7 @@ public interface IValidateCodeTest {
for (String message : theMessages) {
assertTrue(outcome.getMessage().contains(message));
}
assertFalse(outcome.getCodeValidationIssues().isEmpty());
assertFalse(outcome.getIssues().isEmpty());
}
@Test
@ -130,11 +148,7 @@ public interface IValidateCodeTest {
assertEquals(DISPLAY, outcome.getDisplay());
assertNull(outcome.getSeverity());
assertNull(outcome.getMessage());
assertTrue(outcome.getCodeValidationIssues().isEmpty());
assertEquals(CODE, getValueSetProvider().getCode());
assertEquals(DISPLAY, getValueSetProvider().getDisplay());
assertEquals(VALUE_SET_URL, getValueSetProvider().getValueSet());
assertTrue(outcome.getIssues().isEmpty());
}
@Test
@ -147,9 +161,7 @@ public interface IValidateCodeTest {
assertEquals(DISPLAY, outcome.getDisplay());
assertNull(outcome.getSeverity());
assertNull(outcome.getMessage());
assertTrue(outcome.getCodeValidationIssues().isEmpty());
assertEquals(CODE, getCodeSystemProvider().getCode());
assertTrue(outcome.getIssues().isEmpty());
}
@Test
@ -165,10 +177,7 @@ public interface IValidateCodeTest {
assertNull(outcome.getDisplay());
assertNull(outcome.getSeverity());
assertNull(outcome.getMessage());
assertTrue(outcome.getCodeValidationIssues().isEmpty());
assertEquals(CODE, getCodeSystemProvider().getCode());
assertEquals(CODE_SYSTEM, getCodeSystemProvider().getSystem());
assertTrue(outcome.getIssues().isEmpty());
}
@Test
@ -184,15 +193,11 @@ public interface IValidateCodeTest {
assertEquals(DISPLAY, outcome.getDisplay());
assertNull(outcome.getSeverity());
assertNull(outcome.getMessage());
assertTrue(outcome.getCodeValidationIssues().isEmpty());
assertEquals(CODE, getCodeSystemProvider().getCode());
assertEquals(DISPLAY, getCodeSystemProvider().getDisplay());
assertEquals(CODE_SYSTEM, getCodeSystemProvider().getSystem());
assertTrue(outcome.getIssues().isEmpty());
}
@Test
default void validateCode_withCodeSystemError_returnsCorrectly() {
default void validateCode_withCodeSystemErrorWithDiagnosticsWithIssues_returnsCorrectly() {
IBaseOperationOutcome invalidCodeOutcome = getCodeSystemInvalidCodeOutcome();
createCodeSystemReturnParameters(false, null, ERROR_MESSAGE, invalidCodeOutcome);
@ -204,12 +209,12 @@ public interface IValidateCodeTest {
// assertEquals(CODE, outcome.getCode());
assertEquals(ERROR, outcome.getSeverity());
assertEquals(getCodeSystemError(), outcome.getMessage());
assertFalse(outcome.getCodeValidationIssues().isEmpty());
assertFalse(outcome.getIssues().isEmpty());
verifyIssues(invalidCodeOutcome, outcome);
}
@Test
default void validateCode_withCodeSystemErrorAndIssues_returnsCorrectly() {
default void validateCode_withCodeSystemErrorWithDiagnosticsWithoutIssues_returnsCorrectly() {
createCodeSystemReturnParameters(false, null, ERROR_MESSAGE, null);
CodeValidationResult outcome = getService()
@ -223,10 +228,32 @@ public interface IValidateCodeTest {
assertNull(outcome.getDisplay());
assertEquals(ERROR, outcome.getSeverity());
assertEquals(expectedError, outcome.getMessage());
assertFalse(outcome.getCodeValidationIssues().isEmpty());
assertEquals(1, outcome.getCodeValidationIssues().size());
assertEquals(expectedError, outcome.getCodeValidationIssues().get(0).getMessage());
assertEquals(ERROR, outcome.getCodeValidationIssues().get(0).getSeverity());
assertFalse(outcome.getIssues().isEmpty());
assertEquals(1, outcome.getIssues().size());
assertEquals(expectedError, outcome.getIssues().get(0).getDiagnostics());
assertEquals(ERROR, outcome.getIssues().get(0).getSeverity());
}
@Test
default void validateCode_withCodeSystemErrorWithoutDiagnosticsWithIssues_returnsCorrectly() {
IBaseOperationOutcome invalidCodeOutcome = getCodeSystemInvalidCodeOutcome();
createCodeSystemReturnParameters(false, null, null, invalidCodeOutcome);
CodeValidationResult outcome = getService()
.validateCode(null, null, CODE_SYSTEM, CODE, null, null);
String expectedError = getCodeSystemError();
assertNotNull(outcome);
assertEquals(CODE_SYSTEM, outcome.getCodeSystemName());
assertEquals(CODE_SYSTEM_VERSION, outcome.getCodeSystemVersion());
// assertEquals(CODE, outcome.getCode());
assertNull(outcome.getDisplay());
assertEquals(ERROR, outcome.getSeverity());
assertNull(outcome.getMessage());
assertFalse(outcome.getIssues().isEmpty());
assertEquals(1, outcome.getIssues().size());
assertNull(outcome.getIssues().get(0).getDiagnostics());
assertEquals(ERROR, outcome.getIssues().get(0).getSeverity());
}
@Test
@ -242,10 +269,7 @@ public interface IValidateCodeTest {
assertNull(outcome.getDisplay());
assertNull(outcome.getSeverity());
assertNull(outcome.getMessage());
assertTrue(outcome.getCodeValidationIssues().isEmpty());
assertEquals(CODE, getValueSetProvider().getCode());
assertEquals(VALUE_SET_URL, getValueSetProvider().getValueSet());
assertTrue(outcome.getIssues().isEmpty());
}
@Test
@ -261,11 +285,7 @@ public interface IValidateCodeTest {
assertEquals(DISPLAY, outcome.getDisplay());
assertNull(outcome.getSeverity());
assertNull(outcome.getMessage());
assertTrue(outcome.getCodeValidationIssues().isEmpty());
assertEquals(CODE, getValueSetProvider().getCode());
assertEquals(DISPLAY, getValueSetProvider().getDisplay());
assertEquals(VALUE_SET_URL, getValueSetProvider().getValueSet());
assertTrue(outcome.getIssues().isEmpty());
}
@Test
@ -283,13 +303,9 @@ public interface IValidateCodeTest {
assertEquals(DISPLAY, outcome.getDisplay());
assertEquals(ERROR, outcome.getSeverity());
assertEquals(expectedError, outcome.getMessage());
assertEquals(1, outcome.getCodeValidationIssues().size());
assertEquals(expectedError, outcome.getCodeValidationIssues().get(0).getMessage());
assertEquals(ERROR, outcome.getCodeValidationIssues().get(0).getSeverity());
assertEquals(CODE, getValueSetProvider().getCode());
assertEquals(DISPLAY, getValueSetProvider().getDisplay());
assertEquals(VALUE_SET_URL, getValueSetProvider().getValueSet());
assertEquals(1, outcome.getIssues().size());
assertEquals(expectedError, outcome.getIssues().get(0).getDiagnostics());
assertEquals(ERROR, outcome.getIssues().get(0).getSeverity());
}
@Test
@ -306,24 +322,28 @@ public interface IValidateCodeTest {
assertEquals(DISPLAY, outcome.getDisplay());
assertEquals(ERROR, outcome.getSeverity());
assertEquals(getValueSetError(), outcome.getMessage());
assertFalse(outcome.getCodeValidationIssues().isEmpty());
assertFalse(outcome.getIssues().isEmpty());
verifyIssues(invalidCodeOutcome, outcome);
assertEquals(CODE, getValueSetProvider().getCode());
assertEquals(DISPLAY, getValueSetProvider().getDisplay());
assertEquals(VALUE_SET_URL, getValueSetProvider().getValueSet());
}
default void verifyIssues(IBaseOperationOutcome theOperationOutcome, CodeValidationResult theResult) {
List<IValidationSupport.CodeValidationIssue> issues = getCodeValidationIssues(theOperationOutcome);
assertEquals(issues.size(), theResult.getCodeValidationIssues().size());
assertEquals(issues.size(), theResult.getIssues().size());
for (int i = 0; i < issues.size(); i++) {
IValidationSupport.CodeValidationIssue expectedIssue = issues.get(i);
IValidationSupport.CodeValidationIssue actualIssue = theResult.getCodeValidationIssues().get(i);
assertEquals(expectedIssue.getCode(), actualIssue.getCode());
IValidationSupport.CodeValidationIssue actualIssue = theResult.getIssues().get(i);
assertEquals(expectedIssue.getType().getCode(), actualIssue.getType().getCode());
assertEquals(expectedIssue.getSeverity(), actualIssue.getSeverity());
assertEquals(expectedIssue.getCoding(), actualIssue.getCoding());
assertEquals(expectedIssue.getMessage(), actualIssue.getMessage());
assertEquals(expectedIssue.getDetails().getText(), actualIssue.getDetails().getText());
assertEquals(expectedIssue.getDetails().getCodings().size(), actualIssue.getDetails().getCodings().size());
for (int index = 0; index < expectedIssue.getDetails().getCodings().size(); index++) {
IValidationSupport.CodeValidationIssueCoding expectedCoding = expectedIssue.getDetails().getCodings().get(index);
IValidationSupport.CodeValidationIssueCoding actualCoding = actualIssue.getDetails().getCodings().get(index);
assertEquals(expectedCoding.getSystem(), actualCoding.getSystem());
assertEquals(expectedCoding.getCode(), actualCoding.getCode());
}
assertEquals(expectedIssue.getDetails().getText(), actualIssue.getDetails().getText());
assertEquals(expectedIssue.getDiagnostics(), actualIssue.getDiagnostics());
}
}

View File

@ -1,39 +0,0 @@
package org.hl7.fhir.common.hapi.validation;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.rest.server.IResourceProvider;
import org.hl7.fhir.instance.model.api.IBaseParameters;
public interface IValidationProviders {
String CODE_SYSTEM = "http://code.system/url";
String CODE_SYSTEM_VERSION = "1.0.0";
String CODE_SYSTEM_NAME = "Test Code System";
String CODE = "CODE";
String VALUE_SET_URL = "http://value.set/url";
String DISPLAY = "Explanation for code TestCode.";
String LANGUAGE = "en";
String ERROR_MESSAGE = "This is an error message";
interface IMyCodeSystemProvider extends IResourceProvider {
String getCode();
String getSystem();
String getDisplay();
void setException(Exception theException);
void setReturnParams(IBaseParameters theParameters);
}
interface IMyLookupCodeProvider extends IResourceProvider {
String getCode();
String getSystem();
void setLookupCodeResult(IValidationSupport.LookupCodeResult theLookupCodeResult);
}
interface IMyValueSetProvider extends IResourceProvider {
String getCode();
String getSystem();
String getDisplay();
String getValueSet();
void setException(Exception theException);
void setReturnParams(IBaseParameters theParameters);
}
}

View File

@ -7,7 +7,6 @@ import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
@ -16,17 +15,18 @@ import org.hl7.fhir.utilities.validation.ValidationOptions;
import org.junit.jupiter.api.Test;
import org.mockito.quality.Strictness;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import java.util.List;
public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestWithInlineMocks {
final byte[] EXPECTED_BINARY_CONTENT_1 = "dummyBinaryContent1".getBytes();
@ -80,7 +80,7 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW
}
@Test
public void validateCode_normally_resolvesCodeSystemFromValueSet() {
public void validateCode_codeInValueSet_resolvesCodeSystemFromValueSet() {
// setup
IValidationSupport validationSupport = mockValidationSupport();
ValidationSupportContext mockContext = mockValidationSupportContext(validationSupport);
@ -90,8 +90,7 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW
ValueSet valueSet = new ValueSet();
valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0");
valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2");
when(validationSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(valueSet);
when(validationSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenReturn(new IValidationSupport.CodeValidationResult());
when(validationSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenReturn(mock(IValidationSupport.CodeValidationResult.class));
// execute
wrapper.validateCode(new ValidationOptions(), "code0", valueSet);
@ -101,6 +100,26 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW
verify(validationSupport, times(1)).validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), any());
}
@Test
public void validateCode_codeNotInValueSet_doesNotResolveSystem() {
// setup
IValidationSupport validationSupport = mockValidationSupport();
ValidationSupportContext mockContext = mockValidationSupportContext(validationSupport);
VersionCanonicalizer versionCanonicalizer = new VersionCanonicalizer(FhirContext.forR5Cached());
VersionSpecificWorkerContextWrapper wrapper = new VersionSpecificWorkerContextWrapper(mockContext, versionCanonicalizer);
ValueSet valueSet = new ValueSet();
valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0");
valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2");
// execute
wrapper.validateCode(new ValidationOptions(), "code1", valueSet);
// verify
verify(validationSupport, times(1)).validateCodeInValueSet(any(), any(), eq(null), eq("code1"), any(), any());
verify(validationSupport, never()).validateCode(any(), any(), any(), any(), any(), any());
}
@Test
public void isPrimitive_primitive() {
// setup

View File

@ -4,7 +4,6 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks;
import ca.uhn.fhir.model.dstu2.composite.PeriodDt;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
@ -28,10 +27,7 @@ import org.hl7.fhir.dstu2.model.Observation.ObservationStatus;
import org.hl7.fhir.dstu2.model.QuestionnaireResponse;
import org.hl7.fhir.dstu2.model.QuestionnaireResponse.QuestionnaireResponseStatus;
import org.hl7.fhir.dstu2.model.StringType;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -41,9 +37,7 @@ import org.mockito.stubbing.Answer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@ -100,7 +94,7 @@ public class FhirInstanceValidatorDstu2Test extends BaseValidationTestWithInline
if (myValidConcepts.contains(system + "___" + code)) {
retVal = new IValidationSupport.CodeValidationResult().setCode(code);
} else if (myValidSystems.contains(system)) {
return new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage("Unknown code");
return new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage("Unknown code");
} else {
retVal = null;
}

View File

@ -58,7 +58,6 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor;
import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher;
import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
@ -229,10 +228,10 @@ public class FhirInstanceValidatorDstu3Test extends BaseValidationTestWithInline
retVal = new IValidationSupport.CodeValidationResult().setCode(code);
} else if (myValidSystems.contains(system)) {
final String message = "Unknown code (for '" + system + "#" + code + "')";
retVal = new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage(message).setCodeValidationIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(message, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE)));
retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(message).setIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(message, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE)));
} else if (myValidSystemsNotReturningIssues.contains(system)) {
final String message = "Unknown code (for '" + system + "#" + code + "')";
retVal = new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage(message);
retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(message);
} else if (myCodeSystems.containsKey(system)) {
CodeSystem cs = myCodeSystems.get(system);
Optional<ConceptDefinitionComponent> found = cs.getConcept().stream().filter(t -> t.getCode().equals(code)).findFirst();

View File

@ -1,159 +0,0 @@
package org.hl7.fhir.dstu3.hapi.validation;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.common.hapi.validation.IValidationProviders;
import org.hl7.fhir.dstu3.model.BooleanType;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.CodeType;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Parameters;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
public interface IValidateCodeProvidersDstu3 {
@SuppressWarnings("unused")
class MyCodeSystemProviderDstu3 implements IValidationProviders.IMyCodeSystemProvider {
private UriType mySystemUrl;
private CodeType myCode;
private StringType myDisplay;
private Exception myException;
private Parameters myReturnParams;
@Operation(name = "validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = org.hl7.fhir.dstu3.model.BooleanType.class, min = 1),
@OperationParam(name = "message", type = org.hl7.fhir.dstu3.model.StringType.class),
@OperationParam(name = "display", type = org.hl7.fhir.dstu3.model.StringType.class)
})
public org.hl7.fhir.dstu3.model.Parameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) org.hl7.fhir.dstu3.model.IdType theId,
@OperationParam(name = "url", min = 0, max = 1) org.hl7.fhir.dstu3.model.UriType theCodeSystemUrl,
@OperationParam(name = "code", min = 0, max = 1) org.hl7.fhir.dstu3.model.CodeType theCode,
@OperationParam(name = "display", min = 0, max = 1) org.hl7.fhir.dstu3.model.StringType theDisplay
) throws Exception {
mySystemUrl = theCodeSystemUrl;
myCode = theCode;
myDisplay = theDisplay;
if (myException != null) {
throw myException;
}
return myReturnParams;
}
@Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= {
@OperationParam(name = "name", type = org.hl7.fhir.dstu3.model.StringType.class, min = 1),
@OperationParam(name = "version", type = org.hl7.fhir.dstu3.model.StringType.class),
@OperationParam(name = "display", type = org.hl7.fhir.dstu3.model.StringType.class, min = 1),
@OperationParam(name = "abstract", type = org.hl7.fhir.dstu3.model.BooleanType.class, min = 1),
@OperationParam(name = "property", type = org.hl7.fhir.dstu3.model.StringType.class, min = 0, max = OperationParam.MAX_UNLIMITED)
})
public IBaseParameters lookup(
HttpServletRequest theServletRequest,
@OperationParam(name = "code", max = 1) org.hl7.fhir.dstu3.model.CodeType theCode,
@OperationParam(name = "system",max = 1) org.hl7.fhir.dstu3.model.UriType theSystem,
@OperationParam(name = "coding", max = 1) Coding theCoding,
@OperationParam(name = "version", max = 1) org.hl7.fhir.dstu3.model.StringType theVersion,
@OperationParam(name = "displayLanguage", max = 1) org.hl7.fhir.dstu3.model.CodeType theDisplayLanguage,
@OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List<org.hl7.fhir.dstu3.model.CodeType> thePropertyNames,
RequestDetails theRequestDetails
) {
myCode = theCode;
return myReturnParams;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
public void setException(Exception theException) {
myException = theException;
}
@Override
public void setReturnParams(IBaseParameters theParameters) {
myReturnParams = (Parameters) theParameters;
}
@Override
public String getCode() {
return myCode != null ? myCode.getValueAsString() : null;
}
@Override
public String getSystem() {
return mySystemUrl != null ? mySystemUrl.getValueAsString() : null;
}
public String getDisplay() {
return myDisplay != null ? myDisplay.getValue() : null;
}
}
@SuppressWarnings("unused")
class MyValueSetProviderDstu3 implements IValidationProviders.IMyValueSetProvider {
private Exception myException;
private Parameters myReturnParams;
private UriType mySystemUrl;
private UriType myValueSetUrl;
private CodeType myCode;
private StringType myDisplay;
@Operation(name = "validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = org.hl7.fhir.dstu3.model.StringType.class),
@OperationParam(name = "display", type = org.hl7.fhir.dstu3.model.StringType.class)
})
public Parameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) IdType theId,
@OperationParam(name = "url", min = 0, max = 1) org.hl7.fhir.dstu3.model.UriType theValueSetUrl,
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
@OperationParam(name = "system", min = 0, max = 1) UriType theSystem,
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay,
@OperationParam(name = "valueSet") org.hl7.fhir.dstu3.model.ValueSet theValueSet
) throws Exception {
mySystemUrl = theSystem;
myValueSetUrl = theValueSetUrl;
myCode = theCode;
myDisplay = theDisplay;
if (myException != null) {
throw myException;
}
return myReturnParams;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return ValueSet.class;
}
public void setException(Exception theException) {
myException = theException;
}
@Override
public void setReturnParams(IBaseParameters theParameters) {
myReturnParams = (Parameters) theParameters;
}
@Override
public String getCode() {
return myCode != null ? myCode.getValueAsString() : null;
}
@Override
public String getSystem() {
return mySystemUrl != null ? mySystemUrl.getValueAsString() : null;
}
@Override
public String getValueSet() {
return myValueSetUrl != null ? myValueSetUrl.getValueAsString() : null;
}
public String getDisplay() {
return myDisplay != null ? myDisplay.getValue() : null;
}
}
}

View File

@ -41,7 +41,6 @@ import org.hl7.fhir.dstu3.model.Type;
import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
@ -56,6 +55,8 @@ import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity.ERROR;
import static ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity.WARNING;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType.BOOLEAN;
import static org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType.CHOICE;
@ -224,7 +225,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
when(myValSupport.validateCodeInValueSet(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(ValueSet.class)))
.thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0"));
when(myValSupport.validateCodeInValueSet(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(ValueSet.class)))
.thenReturn(new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage("Unknown code"));
.thenReturn(new IValidationSupport.CodeValidationResult().setSeverity(ERROR).setMessage("Unknown code"));
CodeSystem codeSystem = new CodeSystem();
codeSystem.setContent(CodeSystemContentMode.COMPLETE);
@ -246,7 +247,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class)))
.thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT));
when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(String.class)))
.thenReturn(new IValidationSupport.CodeValidationResult().setSeverityCode("warning").setMessage("Unknown code: http://codesystems.com/system / code1"));
.thenReturn(new IValidationSupport.CodeValidationResult().setSeverity(WARNING).setMessage("Unknown code: http://codesystems.com/system / code1"));
QuestionnaireResponse qa;
@ -1034,7 +1035,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class)))
.thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0"));
when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(String.class)))
.thenReturn(new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage("Unknown code"));
.thenReturn(new IValidationSupport.CodeValidationResult().setSeverity(ERROR).setMessage("Unknown code"));
CodeSystem codeSystem = new CodeSystem();
codeSystem.setContent(CodeSystemContentMode.COMPLETE);

Some files were not shown because too many files have changed in this diff Show More