diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java index 71d561c1db3..fdbc4ca31d7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java @@ -440,74 +440,259 @@ public interface IValidationSupport { return "Unknown " + getFhirContext().getVersion().getVersion() + " Validation Support"; } + /** + * Defines codes in system http://hl7.org/fhir/issue-severity. + */ + /* 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 http://hl7.org/fhir/issue-severity. + * @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 http://hl7.org/fhir/issue-type. + * 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 http://hl7.org/fhir/issue-type. + * @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 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 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 http://hl7.org/fhir/tools/CodeSystem/tx-issue-type. + * 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 myCodeValidationIssues; + private List 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 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 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 getIssues() { + if (myIssues == null) { + myIssues = new ArrayList<>(); + } + return myIssues; + } + + public CodeValidationResult setIssues(List 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; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java index f0559373e0a..ce932333840 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java @@ -105,7 +105,6 @@ public abstract class BaseParser implements IParser { private static final Set notEncodeForContainedResource = new HashSet<>(Arrays.asList("security", "versionId", "lastUpdated")); - private FhirTerser.ContainedResources myContainedResources; private boolean myEncodeElementsAppliesToChildResourcesOnly; private final FhirContext myContext; private Collection 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 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 myEncodeElementPaths; private final Set myEncodeElementsAppliesToResourceTypes; private final List myDontEncodeElementPaths; + private FhirTerser.ContainedResources myContainedResources; - public EncodeContext(BaseParser theParser, ParserOptions theParserOptions) { + public EncodeContext( + BaseParser theParser, + ParserOptions theParserOptions, + FhirTerser.ContainedResources theContainedResources) { Collection encodeElements = theParser.myEncodeElements; Collection 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> getCompositeChildrenCache() { return myCompositeChildrenCache; } + + public FhirTerser.ContainedResources getContainedResources() { + return myContainedResources; + } + + public void setContainedResources(FhirTerser.ContainedResources theContainedResources) { + myContainedResources = theContainedResources; + } } protected class CompositeChildElement { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java index 0b25304ee5f..5731742ac1f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java @@ -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 containedResources = getContainedResources().getContainedResources(); + List 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); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java index b75ee9c897f..18572824fee 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java @@ -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; } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java index 7a5aaa021bf..71b83de8d8e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java @@ -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()); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/Repository.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/Repository.java index e859a9ae569..c272656f47a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/Repository.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/Repository.java @@ -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 FHIR search + * + * @param a Bundle type + * @param 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 search( + Class bundleType, Class resourceType, Multimap> searchParameters) { + return this.search(bundleType, resourceType, searchParameters, Collections.emptyMap()); + } + /** * Searches this repository * @@ -264,9 +283,32 @@ public interface Repository { B search( Class bundleType, Class resourceType, - Map> searchParameters, + Multimap> searchParameters, Map headers); + /** + * Searches this repository + * + * @see FHIR search + * + * @param a Bundle type + * @param 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 search( + Class bundleType, + Class resourceType, + Map> searchParameters, + Map headers) { + ArrayListMultimap> multimap = ArrayListMultimap.create(); + searchParameters.forEach(multimap::put); + return this.search(bundleType, resourceType, multimap, headers); + } + // Paging starts here /** diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java index 6eab1ce9ed3..33aedc920e9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java @@ -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 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 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 myResourceList; private IdentityHashMap myResourceToIdMap; private Map 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()) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java new file mode 100644 index 00000000000..09b24657b66 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java @@ -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 myAdapterFactories = new HashSet<>(); + + /** + * Hidden to force shared use of the public INSTANCE. + */ + AdapterManager() {} + + public @Nonnull Optional getAdapter(Object theObject, Class 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); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java new file mode 100644 index 00000000000..36237becd9a --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java @@ -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 Optional adapt(Object theObject, Class 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); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java new file mode 100644 index 00000000000..5eb1b266b49 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java @@ -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 @Nonnull Optional getAdapter(@Nonnull Class theTargetType) { + return AdapterUtils.adapt(this, theTargetType); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java new file mode 100644 index 00000000000..a183705cc53 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java @@ -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. + */ + Optional getAdapter(Object theObject, Class theAdapterType); + + /** + * @return the collection of adapter target types handled by this factory. + */ + Collection> getAdapters(); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java new file mode 100644 index 00000000000..9550ccbdc27 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java @@ -0,0 +1,10 @@ +package ca.uhn.fhir.util.adapters; + +import java.util.Optional; + +/** + * Get an adaptor + */ +public interface IAdapterManager { + Optional getAdapter(Object theTarget, Class theAdapter); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java new file mode 100644 index 00000000000..de9e92df5c4 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java @@ -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. + *

+ * 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. + *

+ *

+ * For implementors, you can support adaptation via two mechanisms: + *

    + *
  • by implementing {@link ca.uhn.fhir.util.adapters.IAdaptable} directly on a class to provide supported adapters, + *
  • 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}.
  • + *
+ * The AdapterUtils.adapt() supports both of these. + *

+ * Inspired by the Eclipse runtime. + */ +package ca.uhn.fhir.util.adapters; diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java new file mode 100644 index 00000000000..ee621533587 --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java @@ -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 Optional getAdapter(Object theObject, Class 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> getAdapters() { + return List.of(Integer.class); + } + } +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java new file mode 100644 index 00000000000..c7dfef8661b --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java @@ -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 Optional getAdapter(@Nonnull Class 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 Optional getAdapter(Object theObject, Class theAdapterType) { + if (theObject instanceof ManagerAdaptableClass && theAdapterType == InterfaceA.class) { + T adapter = theAdapterType.cast(buildInterfaceAWrapper(theObject)); + return Optional.of(adapter); + } + return Optional.empty(); + } + + @Override + public Collection> getAdapters() { + return Set.of(InterfaceA.class); + } + } +} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6403-json-parser-bugs.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6403-json-parser-bugs.yaml new file mode 100644 index 00000000000..f4fe5863e4f --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6403-json-parser-bugs.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6404-fulltext-search-with-lastupdate.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6404-fulltext-search-with-lastupdate.yaml new file mode 100644 index 00000000000..9ada986bc56 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6404-fulltext-search-with-lastupdate.yaml @@ -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. +" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6419-fix-reindex-optimize-storage-all-versions-posgtres.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6419-fix-reindex-optimize-storage-all-versions-posgtres.yaml new file mode 100644 index 00000000000..4d0100526ad --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6419-fix-reindex-optimize-storage-all-versions-posgtres.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6420-fix-reindex-optimize-storage-all-versions-for-a-single-resource.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6420-fix-reindex-optimize-storage-all-versions-for-a-single-resource.yaml new file mode 100644 index 00000000000..ee409dc43ee --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6420-fix-reindex-optimize-storage-all-versions-for-a-single-resource.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6422-fixes-remote-terminology-issues.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6422-fixes-remote-terminology-issues.yaml new file mode 100644 index 00000000000..9b6dc6320ce --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6422-fixes-remote-terminology-issues.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6440-fix-hooks-not-called-for-precheck-for-cached-search.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6440-fix-hooks-not-called-for-precheck-for-cached-search.yaml new file mode 100644 index 00000000000..86f16018012 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6440-fix-hooks-not-called-for-precheck-for-cached-search.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6445-repository-api-multimap.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6445-repository-api-multimap.yaml new file mode 100644 index 00000000000..841287f278d --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6445-repository-api-multimap.yaml @@ -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`" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/upgrade.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/upgrade.md index 3ddb3ad568b..d75c669499c 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/upgrade.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/upgrade.md @@ -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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/clinical_reasoning/questionnaires.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/clinical_reasoning/questionnaires.md index 8b2426a7d65..d409d254582 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/clinical_reasoning/questionnaires.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/clinical_reasoning/questionnaires.md @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 4294a18918d..340f9846a1d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -981,12 +981,10 @@ public abstract class BaseHapiFhirDao 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 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 extends BaseStora theEntity.setContentText(parseContentTextIntoWords(theContext, theResource)); if (myStorageSettings.isAdvancedHSearchIndexing()) { ExtendedHSearchIndexData hSearchIndexData = - myFulltextSearchSvc.extractLuceneIndexData(theResource, theNewParams); + myFulltextSearchSvc.extractLuceneIndexData(theResource, theEntity, theNewParams); theEntity.setLuceneIndexData(hSearchIndexData); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index f1d52bbd30b..66e7f928f5f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -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 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 historyEntities = myResourceHistoryTableDao.findForResourceIdAndReturnEntitiesAndFetchProvenance( - PageRequest.of(page, pageSize), entity.getId(), historyEntity.getVersion()); + pageRequest, entity.getId(), historyEntity.getVersion()); + for (ResourceHistoryTable next : historyEntities) { reindexOptimizeStorageHistoryEntity(entity, next); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java index 0aedafefed7..52692c34124 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java @@ -205,7 +205,7 @@ public abstract class BaseHapiFhirSystemDao extends B * * However, for realistic average workloads, this should reduce the number of round trips. */ - if (idChunk.size() >= 2) { + if (!idChunk.isEmpty()) { List entityChunk = prefetchResourceTableHistoryAndProvenance(idChunk); if (thePreFetchIndexes) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java index 0bccd1b6c67..5e3037a60b8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java index 52dd7589947..0b795fb36a8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java @@ -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. diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchClauseBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchClauseBuilder.java index 37a2a8830ef..e33b4c293d3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchClauseBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchClauseBuilder.java @@ -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 * *

Example 1 ('eq' prefix/empty): http://fhirserver/Observation?date=eq2020 * would generate the following search clause diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractor.java index 63642b2b4f1..86f99a60316 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractor.java @@ -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> linkPathToParamName = new HashMap<>(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchSearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchSearchBuilder.java index 79029f95585..e67d0907705 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchSearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchSearchBuilder.java @@ -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? *

diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java index 477999b3d3e..f169fba81d8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java @@ -605,12 +605,12 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { .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; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java index e9f07b798a5..f443a55be83 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java @@ -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, diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java index 1770bcd910c..3216b970201 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java +++ b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java @@ -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() { diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4ElasticsearchIT.java b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4ElasticsearchIT.java index 2bfc2ae1b2a..1ef05d8abf4 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4ElasticsearchIT.java +++ b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4ElasticsearchIT.java @@ -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 diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java index 33a288ee811..ee0e08ef7ea 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java @@ -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"); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedHSearchIndexData.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedHSearchIndexData.java index 9af2aabf4de..ef8f9967286 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedHSearchIndexData.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedHSearchIndexData.java @@ -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 BiConsumer ifNotContained(BiConsumer theIndexWriter) { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java index f73670fbddc..2eb8a60aa2e 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java @@ -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 diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java index 636ffd02b40..6b10598174b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java @@ -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; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index 4c9464fa4d1..4668a4f590a 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -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(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java index 4ad723edfa3..f835c300c19 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java @@ -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); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java index a0f80459464..14fa8d5dc88 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java @@ -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 expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate); - - for (int i = 0; i < numberOfPatientsToCreate; i++) { - Patient patient = new Patient(); - patient.getText().setDivAsString("

AAAS

FOO

CCC
"); - expectedActivePatientIds.add(myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless().getIdPart()); - } - - { - Patient patient = new Patient(); - patient.getText().setDivAsString("
AAAB

FOO

CCC
"); - myPatientDao.create(patient, mySrd); - } - { - Patient patient = new Patient(); - patient.getText().setDivAsString("
ZZYZXY
"); - 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 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("
ABC
"); - 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( - "
FINDME
" - ); - 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 ids = provider.getAllResourceIds(); - assertEquals(1, ids.size()); - } - - @Test - public void testLuceneNarrativeSearchQueryIntersectingJpaQuery() { - final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10; - List 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("
AAAS

FOO

CCC
"); - activePatient.setActive(true); - String patientId = myPatientDao.create(activePatient, mySrd).getId().toUnqualifiedVersionless().getIdPart(); - expectedActivePatientIds.add(patientId); - - Patient nonActivePatient = new Patient(); - nonActivePatient.getText().setDivAsString("
AAAS

FOO

CCC
"); - 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 resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds(); - - assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds); - } - - @Test - public void testLuceneContentSearchQueryIntersectingJpaQuery() { - final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10; - final String patientFamilyName = "Flanders"; - List 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 resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds(); - - assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds); - } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 1ddb1cca9d3..8f3e849719f 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -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(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/reindex/ReindexTaskTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/reindex/ReindexTaskTest.java index c04a40f1ac7..290dfcdf9b4 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/reindex/ReindexTaskTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/reindex/ReindexTaskTest.java @@ -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 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 diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/search/ExtendedHSearchResourceProjectionTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/search/ExtendedHSearchResourceProjectionTest.java index 8ec02d2d8d9..17b41a4485c 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/search/ExtendedHSearchResourceProjectionTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/search/ExtendedHSearchResourceProjectionTest.java @@ -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()); } - - - - } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ValidateCodeOperationWithRemoteTerminologyR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateCodeWithRemoteTerminologyR4Test.java similarity index 59% rename from hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ValidateCodeOperationWithRemoteTerminologyR4Test.java rename to hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateCodeWithRemoteTerminologyR4Test.java index 4da204217f0..3787c611eb7 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ValidateCodeOperationWithRemoteTerminologyR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateCodeWithRemoteTerminologyR4Test.java @@ -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 myCodeSystemProvider; + private IValidationProviders.MyValidationProvider 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 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 find(@RequiredParam(name = "url") UriParam theUrlParam) { - assert myReturnCodeSystems != null; - return myReturnCodeSystems; - } - - @Override - public Class 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 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 find(@RequiredParam(name = "url") UriParam theUrlParam) { - assert myReturnValueSets != null; - return myReturnValueSets; - } - - @Override - public Class 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); } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java new file mode 100644 index 00000000000..79a656db39c --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java @@ -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. OntoServer. + * 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 myCodeSystemProvider; + private IValidationProviders.MyValidationProvider 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 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 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 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 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 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(); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml b/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml index dcada7fec76..f52688d2cbc 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml @@ -31,7 +31,7 @@ - +--> diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/profile-encounter-custom.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/profile-encounter-custom.json new file mode 100644 index 00000000000..a553a61a1c2 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/profile-encounter-custom.json @@ -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 + } + ] + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-encounter-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-encounter-status.json new file mode 100644 index 00000000000..2399dc870ec --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-encounter-status.json @@ -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" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v2-0203.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v2-0203.json new file mode 100644 index 00000000000..10747c14ee3 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v2-0203.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v3-ActCode.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v3-ActCode.json new file mode 100644 index 00000000000..b692847e0fb --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v3-ActCode.json @@ -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" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-encounter-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-encounter-status.json new file mode 100644 index 00000000000..2399dc870ec --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-encounter-status.json @@ -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" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-identifier-type.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-identifier-type.json new file mode 100644 index 00000000000..b0767dc2f18 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-identifier-type.json @@ -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'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-v3-ActEncounterCode.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-v3-ActEncounterCode.json new file mode 100644 index 00000000000..083dbffae43 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-v3-ActEncounterCode.json @@ -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" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-ICD9CM.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-ICD9CM.json new file mode 100644 index 00000000000..831ac6660fa --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-ICD9CM.json @@ -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'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-observation-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-observation-status.json new file mode 100644 index 00000000000..7914321876c --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-observation-status.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-codes.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-codes.json new file mode 100644 index 00000000000..4571362033f --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-codes.json @@ -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'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-observation-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-observation-status.json new file mode 100644 index 00000000000..7914321876c --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-observation-status.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure-slicing.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure-slicing.json new file mode 100644 index 00000000000..8bc05c70cf0 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure-slicing.json @@ -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 + } + ] + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure.json new file mode 100644 index 00000000000..5315694dece --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure.json @@ -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 + } + ] + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-absent-or-unknown.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-absent-or-unknown.json new file mode 100644 index 00000000000..4d7b20f0881 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-absent-or-unknown.json @@ -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" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-event-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-event-status.json new file mode 100644 index 00000000000..620624a991e --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-event-status.json @@ -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" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-invalid.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-invalid.json new file mode 100644 index 00000000000..f6a86048d6e --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-invalid.json @@ -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'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-valid.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-valid.json new file mode 100644 index 00000000000..a602bfda9f0 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-valid.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-absent-or-unknown-procedure.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-absent-or-unknown-procedure.json new file mode 100644 index 00000000000..aaee02a0023 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-absent-or-unknown-procedure.json @@ -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" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-event-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-event-status.json new file mode 100644 index 00000000000..aaad08b83e8 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-event-status.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid-slice.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid-slice.json new file mode 100644 index 00000000000..4dcb4791944 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid-slice.json @@ -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'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid.json new file mode 100644 index 00000000000..fac3785fe2d --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid.json @@ -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'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-valid.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-valid.json new file mode 100644 index 00000000000..4554379edad --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-valid.json @@ -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" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StandardQueriesLuceneTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StandardQueriesLuceneTest.java index 814b342aab7..31ce2f00f93 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StandardQueriesLuceneTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StandardQueriesLuceneTest.java @@ -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 myObservationDao; @Autowired + @Qualifier("myPatientDaoR4") + protected IFhirResourceDaoPatient myPatientDao; + @Autowired + private IFhirSystemDao mySystemDao; + @Autowired + private IResourceReindexingSvc myResourceReindexingSvc; + @Autowired + protected ISearchCoordinatorSvc mySearchCoordinatorSvc; + @Autowired + protected ISearchParamRegistry mySearchParamRegistry; + @Autowired + private IBulkDataExportJobSchedulingHelper myBulkDataScheduleHelper; + @Autowired IFhirResourceDao myPractitionerDao; @Autowired IFhirResourceDao 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 diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/ILuceneSearchR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/ILuceneSearchR4Test.java new file mode 100644 index 00000000000..9dae86edcf6 --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/ILuceneSearchR4Test.java @@ -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 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 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 stream; + List list; + Optional 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 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("
ABC
"); + 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 expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate); + + SystemRequestDetails requestDetails = new SystemRequestDetails(); + + @SuppressWarnings("unchecked") + IFhirResourceDao 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("
AAAS

FOO

CCC
"); + activePatient.setActive(true); + String patientId = patientDao.create(activePatient, requestDetails).getId().toUnqualifiedVersionless().getIdPart(); + expectedActivePatientIds.add(patientId); + + Patient nonActivePatient = new Patient(); + nonActivePatient.getText().setDivAsString("
AAAS

FOO

CCC
"); + 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 resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds(); + + assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds); + } + + @Test + default void testLuceneContentSearchQueryIntersectingJpaQuery() { + final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10; + final String patientFamilyName = "Flanders"; + List expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate); + + SystemRequestDetails requestDetails = new SystemRequestDetails(); + + @SuppressWarnings("unchecked") + IFhirResourceDao 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 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 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( + "
FINDME
" + ); + 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 ids = provider.getAllResourceIds(); + assertEquals(1, ids.size()); + } + + @Test + default void testSearchNarrativeWithLuceneSearch() { + final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10; + List expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate); + + SystemRequestDetails requestDetails = new SystemRequestDetails(); + + @SuppressWarnings("unchecked") + IFhirResourceDao patientDao = getResourceDao("Patient"); + + + for (int i = 0; i < numberOfPatientsToCreate; i++) { + Patient patient = new Patient(); + patient.getText().setDivAsString("
AAAS

FOO

CCC
"); + expectedActivePatientIds.add(patientDao.create(patient, requestDetails).getId().toUnqualifiedVersionless().getIdPart()); + } + + { + Patient patient = new Patient(); + patient.getText().setDivAsString("
AAAB

FOO

CCC
"); + patientDao.create(patient, requestDetails); + } + { + Patient patient = new Patient(); + patient.getText().setDivAsString("
ZZYZXY
"); + 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 resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds(); + + assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds); + } + +} diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractorTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractorTest.java index cb7ae2cf928..46a0db47ff3 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractorTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractorTest.java @@ -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 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 dIndexData = indexData.getDateIndexData().get("Date"); assertThat(dIndexData).hasSize(0); Set qIndexData = indexData.getQuantityIndexData().get("Quantity"); assertThat(qIndexData).hasSize(0); - } @Override diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java index 39d75793f28..6670ab15a4c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java @@ -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; } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java new file mode 100644 index 00000000000..2a670d15811 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java @@ -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); + } +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/v1/ReindexV1Config.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/v1/ReindexV1Config.java index fe8f736aef5..4dd95638937 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/v1/ReindexV1Config.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/v1/ReindexV1Config.java @@ -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(); } diff --git a/hapi-fhir-storage-test-utilities/src/main/java/ca/uhn/fhir/storage/test/DaoTestDataBuilder.java b/hapi-fhir-storage-test-utilities/src/main/java/ca/uhn/fhir/storage/test/DaoTestDataBuilder.java index cd7f6019568..2cb4ce83e53 100644 --- a/hapi-fhir-storage-test-utilities/src/main/java/ca/uhn/fhir/storage/test/DaoTestDataBuilder.java +++ b/hapi-fhir-storage-test-utilities/src/main/java/ca/uhn/fhir/storage/test/DaoTestDataBuilder.java @@ -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; } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java index 25100484649..31d69a14067 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java @@ -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 diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java index 87a33805db6..478d54e8e49 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java @@ -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()); } /** diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java index d9797d85e69..ca5cb382a14 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java @@ -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()); } diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java new file mode 100644 index 00000000000..d9933708e47 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java @@ -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); +

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 implements IMyValidationProvider { + private final Map myExceptionMap = new HashMap<>(); + private boolean myShouldThrowExceptionForResourceNotFound = true; + private final Map myTerminologyResponseMap = new HashMap<>(); + private final Map 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 getParameterType(); + + @Override + public

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 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); + } +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java new file mode 100644 index 00000000000..11c5244df41 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java @@ -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 { + @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 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 getResourceType() { + return CodeSystem.class; + } + @Override + Class 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 { + @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 getResourceType() { + return ValueSet.class; + } + @Override + Class 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; + } + } +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/IValidateCodeProvidersR4.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java similarity index 55% rename from hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/IValidateCodeProvidersR4.java rename to hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java index fd03a8163ff..87a3dacb1fc 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/IValidateCodeProvidersR4.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java @@ -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 { - @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 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 getResourceType() { return CodeSystem.class; } - public void setException(Exception theException) { - myException = theException; - } @Override - public void setReturnParams(IBaseParameters theParameters) { - myReturnParams = (Parameters) theParameters; + Class 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 { - @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 getResourceType() { return ValueSet.class; } - public void setException(Exception theException) { - myException = theException; + @Override + Class 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; } } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java index 1b12e20ec62..7a05e197ae6 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java @@ -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, diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java index 67e553e3d3b..a617e04c41b 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java @@ -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, diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java index 370f8b423dd..398eacc52a2 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java @@ -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 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 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); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java index 1898292c451..265debed058 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java @@ -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, diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportUtils.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportUtils.java index 7321f33d8c8..7093ab2a7ff 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportUtils.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportUtils.java @@ -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); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java index f0f3f41e7f9..393d8ce1dc5 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java @@ -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 getIssuesForCodeValidation( - List codeValidationIssues) { - List issues = new ArrayList<>(); + List theIssues) { + List 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 diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java index bb7eaf1c17b..eac448ad0eb 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java @@ -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()); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java index 5ba79bd3e6f..95c4fd7d3b9 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java @@ -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; diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyValidateCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyValidateCodeTest.java index cb6bb02ac07..21c5d0dc5e7 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyValidateCodeTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyValidateCodeTest.java @@ -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 getCodeValidationIssues(IBaseOperationOutcome theOperationOutcome) { // this method should be removed once support for issues is fully implemented across all validator types Optional> 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> 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> 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> 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."); + } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidateCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidateCodeTest.java index 52dbf1177a8..53411b440fe 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidateCodeTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidateCodeTest.java @@ -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 theResourceClass) { + return getOutcome(theResourceClass, "/terminology/OperationOutcome-CodeSystem-invalid-code.json"); + } + default IBaseOperationOutcome getValueSetInvalidCodeOutcome(Class theResourceClass) { + return getOutcome(theResourceClass, "/terminology/OperationOutcome-ValueSet-invalid-code.json"); + } + default IBaseOperationOutcome getValueSetCustomDetailCodeOutcome(Class theResourceClass) { + return getOutcome(theResourceClass, "/terminology/OperationOutcome-ValueSet-custom-issue-detail.json"); + } + default IBaseOperationOutcome getOutcome(Class 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 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()); } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidationProviders.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidationProviders.java deleted file mode 100644 index 1537f8e5c00..00000000000 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidationProviders.java +++ /dev/null @@ -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); - } -} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java index 57aae8d96f9..cbc79fadc98 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java @@ -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 diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu2/hapi/validation/FhirInstanceValidatorDstu2Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu2/hapi/validation/FhirInstanceValidatorDstu2Test.java index da73c0be800..5a53ba0ac3d 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu2/hapi/validation/FhirInstanceValidatorDstu2Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu2/hapi/validation/FhirInstanceValidatorDstu2Test.java @@ -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; } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java index ad6276f2761..cfbeca68626 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java @@ -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 found = cs.getConcept().stream().filter(t -> t.getCode().equals(code)).findFirst(); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/IValidateCodeProvidersDstu3.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/IValidateCodeProvidersDstu3.java deleted file mode 100644 index 0c639e310ee..00000000000 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/IValidateCodeProvidersDstu3.java +++ /dev/null @@ -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 thePropertyNames, - RequestDetails theRequestDetails - ) { - myCode = theCode; - return myReturnParams; - } - - @Override - public Class 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 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; - } - } -} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java index 7804b9df10f..57591b31e76 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java @@ -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); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java index 6e98c4b31a9..56bc892c3d4 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java @@ -12,9 +12,9 @@ import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; import jakarta.servlet.http.HttpServletRequest; import org.hl7.fhir.common.hapi.validation.IRemoteTerminologyLookupCodeTest; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.dstu3.model.BooleanType; import org.hl7.fhir.dstu3.model.CodeSystem; @@ -164,8 +164,6 @@ public class RemoteTerminologyLookupCodeDstu3Test implements IRemoteTerminologyL @SuppressWarnings("unused") static class MyLookupCodeProviderDstu3 implements IValidationProviders.IMyLookupCodeProvider { - private UriType mySystemUrl; - private CodeType myCode; private LookupCodeResult myLookupCodeResult; @Override @@ -190,8 +188,6 @@ public class RemoteTerminologyLookupCodeDstu3Test implements IRemoteTerminologyL @OperationParam(name= " property", max = OperationParam.MAX_UNLIMITED) List thePropertyNames, RequestDetails theRequestDetails ) { - myCode = theCode; - mySystemUrl = theSystem; if (theSystem == null) { throw new InvalidRequestException(MessageFormat.format(MESSAGE_RESPONSE_INVALID, theCode)); } @@ -205,15 +201,5 @@ public class RemoteTerminologyLookupCodeDstu3Test implements IRemoteTerminologyL public Class getResourceType() { return CodeSystem.class; } - - @Override - public String getCode() { - return myCode != null ? myCode.getValueAsString() : null; - } - - @Override - public String getSystem() { - return mySystemUrl != null ? mySystemUrl.getValueAsString() : null; - } } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeWithResponseFileDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeWithResponseFileDstu3Test.java index 48a99f260d0..4817542ef4d 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeWithResponseFileDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeWithResponseFileDstu3Test.java @@ -5,13 +5,10 @@ import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.LookupCodeRequest; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; -import ca.uhn.fhir.util.ClasspathUtil; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersDstu3; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; -import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,13 +16,15 @@ import org.junit.jupiter.api.extension.RegisterExtension; import java.util.List; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_LOOKUP; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; public class RemoteTerminologyLookupCodeWithResponseFileDstu3Test { private static final FhirContext ourCtx = FhirContext.forDstu3Cached(); - private IValidateCodeProvidersDstu3.MyCodeSystemProviderDstu3 myCodeSystemProvider; + private IValidationProvidersDstu3.MyCodeSystemProviderDstu3 myCodeSystemProvider; @RegisterExtension public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); @@ -36,7 +35,7 @@ public class RemoteTerminologyLookupCodeWithResponseFileDstu3Test { String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl); mySvc.addClientInterceptor(new LoggingInterceptor(false).setLogRequestSummary(true).setLogResponseSummary(true)); - myCodeSystemProvider = new IValidateCodeProvidersDstu3.MyCodeSystemProviderDstu3(); + myCodeSystemProvider = new IValidationProvidersDstu3.MyCodeSystemProviderDstu3(); ourRestfulServerExtension.getRestfulServer().registerProviders(myCodeSystemProvider); } @@ -47,13 +46,10 @@ public class RemoteTerminologyLookupCodeWithResponseFileDstu3Test { } @Test void lookupCode_withParametersOutput_convertsCorrectly() { - String paramsAsString = ClasspathUtil.loadResource("/terminology/CodeSystem-lookup-output-with-subproperties.json"); - IBaseResource baseResource = ourCtx.newJsonParser().parseResource(paramsAsString); - assertTrue(baseResource instanceof Parameters); - Parameters resultParameters = (Parameters) baseResource; - myCodeSystemProvider.setReturnParams(resultParameters); + String outputFile ="/terminology/CodeSystem-lookup-output-with-subproperties.json"; + IBaseParameters resultParameters = myCodeSystemProvider.addTerminologyResponse(OPERATION_LOOKUP, CODE_SYSTEM, CODE, ourCtx, outputFile); - LookupCodeRequest request = new LookupCodeRequest(IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, null, List.of("interfaces")); + LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, null, List.of("interfaces")); // test IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, request); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyValidateCodeDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyValidateCodeDstu3Test.java index af4f39f0926..2f573ad3e5f 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyValidateCodeDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyValidateCodeDstu3Test.java @@ -4,16 +4,18 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; -import ca.uhn.fhir.util.ClasspathUtil; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersDstu3; import org.hl7.fhir.common.hapi.validation.IRemoteTerminologyValidateCodeTest; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.dstu3.model.BooleanType; +import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.OperationOutcome; import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.Resource; 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.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.jupiter.api.AfterEach; @@ -22,6 +24,11 @@ import org.junit.jupiter.api.extension.RegisterExtension; import java.util.List; +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.ERROR_MESSAGE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.VALUE_SET_URL; import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM; import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET; @@ -38,8 +45,8 @@ public class RemoteTerminologyValidateCodeDstu3Test implements IRemoteTerminolog private static final FhirContext ourCtx = FhirContext.forDstu3Cached(); @RegisterExtension public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); - private IValidateCodeProvidersDstu3.MyCodeSystemProviderDstu3 myCodeSystemProvider; - private IValidateCodeProvidersDstu3.MyValueSetProviderDstu3 myValueSetProvider; + private IValidationProviders.MyValidationProvider myCodeSystemProvider; + private IValidationProviders.MyValidationProvider myValueSetProvider; private RemoteTerminologyServiceValidationSupport mySvc; private String myCodeSystemError, myValueSetError; @@ -48,14 +55,14 @@ public class RemoteTerminologyValidateCodeDstu3Test implements IRemoteTerminolog String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); myCodeSystemError = ourCtx.getLocalizer().getMessage( RemoteTerminologyServiceValidationSupport.class, - ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, baseUrl, IValidationProviders.ERROR_MESSAGE); + ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, CODE_SYSTEM, CODE, baseUrl, ERROR_MESSAGE); myValueSetError = ourCtx.getLocalizer().getMessage( RemoteTerminologyServiceValidationSupport.class, - ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET, IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, IValidationProviders.VALUE_SET_URL, baseUrl, IValidationProviders.ERROR_MESSAGE); + ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET, CODE_SYSTEM, CODE, VALUE_SET_URL, baseUrl, ERROR_MESSAGE); mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl); mySvc.addClientInterceptor(new LoggingInterceptor(false).setLogRequestSummary(true).setLogResponseSummary(true)); - myCodeSystemProvider = new IValidateCodeProvidersDstu3.MyCodeSystemProviderDstu3(); - myValueSetProvider = new IValidateCodeProvidersDstu3.MyValueSetProviderDstu3(); + myCodeSystemProvider = new IValidationProvidersDstu3.MyCodeSystemProviderDstu3(); + myValueSetProvider = new IValidationProvidersDstu3.MyValueSetProviderDstu3(); ourRestfulServerExtension.getRestfulServer().registerProviders(myCodeSystemProvider, myValueSetProvider); } @@ -82,45 +89,40 @@ public class RemoteTerminologyValidateCodeDstu3Test implements IRemoteTerminolog } @Override - public IValidateCodeProvidersDstu3.MyCodeSystemProviderDstu3 getCodeSystemProvider() { + public IValidationProviders.IMyValidationProvider getCodeSystemProvider() { return myCodeSystemProvider; } @Override - public IValidateCodeProvidersDstu3.MyValueSetProviderDstu3 getValueSetProvider() { + public IValidationProviders.IMyValidationProvider getValueSetProvider() { return myValueSetProvider; } @Override public IBaseOperationOutcome getCodeSystemInvalidCodeOutcome() { - return ClasspathUtil.loadResource(getService().getFhirContext(), OperationOutcome.class, "/terminology/OperationOutcome-CodeSystem-invalid-code.json"); + return getCodeSystemInvalidCodeOutcome(OperationOutcome.class); } @Override public IBaseOperationOutcome getValueSetInvalidCodeOutcome() { - return ClasspathUtil.loadResource(getService().getFhirContext(), OperationOutcome.class, "/terminology/OperationOutcome-ValueSet-invalid-code.json"); + return getValueSetInvalidCodeOutcome(OperationOutcome.class); + } + + @Override + public IBaseOperationOutcome getValueSetCustomDetailCodeOutcome() { + return getValueSetCustomDetailCodeOutcome(OperationOutcome.class); } @Override public Parameters createParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) { Parameters parameters = new Parameters(); parameters.addParameter().setName("result").setValue(new BooleanType(theResult)); - parameters.addParameter().setName("code").setValue(new StringType(IValidationProviders.CODE)); - parameters.addParameter().setName("system").setValue(new UriType(IValidationProviders.CODE_SYSTEM)); - parameters.addParameter().setName("version").setValue(new StringType(IValidationProviders.CODE_SYSTEM_VERSION)); + parameters.addParameter().setName("code").setValue(new StringType(CODE)); + parameters.addParameter().setName("system").setValue(new UriType(CODE_SYSTEM)); + parameters.addParameter().setName("version").setValue(new StringType(CODE_SYSTEM_VERSION)); parameters.addParameter().setName("display").setValue(new StringType(theDisplay)); parameters.addParameter().setName("message").setValue(new StringType(theMessage)); parameters.addParameter().setName("issues").setResource((Resource) theIssuesResource); return parameters; } - - @Override - public void createCodeSystemReturnParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) { - myCodeSystemProvider.setReturnParams(createParameters(theResult, theDisplay, theMessage, theIssuesResource)); - } - - @Override - public void createValueSetReturnParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) { - myValueSetProvider.setReturnParams(createParameters(theResult, theDisplay, theMessage, theIssuesResource)); - } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index d03c3aa974d..fb46f7f8008 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -307,10 +307,10 @@ public class FhirInstanceValidatorR4Test extends BaseValidationTestWithInlineMoc 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 { retVal = myDefaultValidationSupport.validateCode(new ValidationSupportContext(myDefaultValidationSupport), options, system, code, display, valueSetUrl); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java index d67966df6d4..382f621f547 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java @@ -11,9 +11,10 @@ import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 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 jakarta.servlet.http.HttpServletRequest; import org.hl7.fhir.common.hapi.validation.IRemoteTerminologyLookupCodeTest; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseParameters; @@ -52,7 +53,7 @@ public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLook @RegisterExtension public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); private final RemoteTerminologyServiceValidationSupport mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx); - private IValidateCodeProvidersR4.MyCodeSystemProviderR4 myCodeSystemProvider; + private IValidationProvidersR4.MyCodeSystemProviderR4 myCodeSystemProvider; private MyLookupCodeProviderR4 myLookupCodeProviderR4; @BeforeEach @@ -60,7 +61,7 @@ public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLook String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); mySvc.setBaseUrl(baseUrl); mySvc.addClientInterceptor(new LoggingInterceptor(true)); - myCodeSystemProvider = new IValidateCodeProvidersR4.MyCodeSystemProviderR4(); + myCodeSystemProvider = new IValidationProvidersR4.MyCodeSystemProviderR4(); myLookupCodeProviderR4 = new MyLookupCodeProviderR4(); ourRestfulServerExtension.getRestfulServer().registerProviders(myCodeSystemProvider, myLookupCodeProviderR4); } @@ -166,8 +167,6 @@ public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLook @SuppressWarnings("unused") static class MyLookupCodeProviderR4 implements IValidationProviders.IMyLookupCodeProvider { - private UriType mySystemUrl; - private CodeType myCode; private LookupCodeResult myLookupCodeResult; @Override @@ -192,8 +191,6 @@ public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLook @OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List thePropertyNames, RequestDetails theRequestDetails ) { - myCode = theCode; - mySystemUrl = theSystem; if (theSystem == null) { throw new InvalidRequestException(MessageFormat.format(MESSAGE_RESPONSE_INVALID, theCode)); } @@ -206,15 +203,5 @@ public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLook public Class getResourceType() { return CodeSystem.class; } - - @Override - public String getCode() { - return myCode != null ? myCode.getValueAsString() : null; - } - - @Override - public String getSystem() { - return mySystemUrl != null ? mySystemUrl.getValueAsString() : null; - } } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeWithResponseFileR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeWithResponseFileR4Test.java index 37eba91d0ca..a0896bd8c4e 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeWithResponseFileR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeWithResponseFileR4Test.java @@ -5,12 +5,9 @@ import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.LookupCodeRequest; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; -import ca.uhn.fhir.util.ClasspathUtil; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersR4; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -19,13 +16,15 @@ import org.junit.jupiter.api.extension.RegisterExtension; import java.util.List; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_LOOKUP; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; public class RemoteTerminologyLookupCodeWithResponseFileR4Test { private static final FhirContext ourCtx = FhirContext.forR4Cached(); - private IValidateCodeProvidersR4.MyCodeSystemProviderR4 myCodeSystemProvider; + private IValidationProvidersR4.MyCodeSystemProviderR4 myCodeSystemProvider; @RegisterExtension public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); @@ -36,7 +35,7 @@ public class RemoteTerminologyLookupCodeWithResponseFileR4Test { String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl); mySvc.addClientInterceptor(new LoggingInterceptor(false).setLogRequestSummary(true).setLogResponseSummary(true)); - myCodeSystemProvider = new IValidateCodeProvidersR4.MyCodeSystemProviderR4(); + myCodeSystemProvider = new IValidationProvidersR4.MyCodeSystemProviderR4(); ourRestfulServerExtension.getRestfulServer().registerProviders(myCodeSystemProvider); } @@ -48,13 +47,10 @@ public class RemoteTerminologyLookupCodeWithResponseFileR4Test { @Test void lookupCode_withParametersOutput_convertsCorrectly() { - String paramsAsString = ClasspathUtil.loadResource("/terminology/CodeSystem-lookup-output-with-subproperties.json"); - IBaseResource baseResource = ourCtx.newJsonParser().parseResource(paramsAsString); - assertTrue(baseResource instanceof Parameters); - Parameters resultParameters = (Parameters) baseResource; - myCodeSystemProvider.setReturnParams(resultParameters); + String outputFile ="/terminology/CodeSystem-lookup-output-with-subproperties.json"; + IBaseParameters resultParameters = myCodeSystemProvider.addTerminologyResponse(OPERATION_LOOKUP, CODE_SYSTEM, CODE, ourCtx, outputFile); - LookupCodeRequest request = new LookupCodeRequest(IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, null, List.of("interfaces")); + LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, null, List.of("interfaces")); // test IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, request); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyValidateCodeR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyValidateCodeR4Test.java index 08f6c251869..ffd8045a8a5 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyValidateCodeR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyValidateCodeR4Test.java @@ -2,8 +2,8 @@ package org.hl7.fhir.r4.validation; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.ConceptValidationOptions; -import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult; +import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.parser.IJsonLikeParser; import ca.uhn.fhir.rest.client.api.IClientInterceptor; import ca.uhn.fhir.rest.client.api.IHttpRequest; @@ -13,11 +13,11 @@ import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; -import ca.uhn.fhir.util.ClasspathUtil; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersR4; import ca.uhn.fhir.util.ParametersUtil; import com.google.common.collect.Lists; import org.hl7.fhir.common.hapi.validation.IRemoteTerminologyValidateCodeTest; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -39,6 +39,12 @@ import java.util.Collections; import java.util.List; import java.util.stream.Stream; +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.assertj.core.api.Assertions.assertThat; import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM; import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET; @@ -61,8 +67,8 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa private static final FhirContext ourCtx = FhirContext.forR4Cached(); @RegisterExtension public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); - private IValidateCodeProvidersR4.MyCodeSystemProviderR4 myCodeSystemProvider; - private IValidateCodeProvidersR4.MyValueSetProviderR4 myValueSetProvider; + private IValidationProviders.IMyValidationProvider myCodeSystemProvider; + private IValidationProviders.IMyValidationProvider myValueSetProvider; private RemoteTerminologyServiceValidationSupport mySvc; private String myCodeSystemError, myValueSetError; @@ -71,14 +77,14 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); myCodeSystemError = ourCtx.getLocalizer().getMessage( RemoteTerminologyServiceValidationSupport.class, - ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, baseUrl, IValidationProviders.ERROR_MESSAGE); + ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, CODE_SYSTEM, CODE, baseUrl, ERROR_MESSAGE); myValueSetError = ourCtx.getLocalizer().getMessage( RemoteTerminologyServiceValidationSupport.class, - ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET, IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, IValidationProviders.VALUE_SET_URL, baseUrl, IValidationProviders.ERROR_MESSAGE); + ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET, CODE_SYSTEM, CODE, VALUE_SET_URL, baseUrl, ERROR_MESSAGE); mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl); mySvc.addClientInterceptor(new LoggingInterceptor(false).setLogRequestSummary(true).setLogResponseSummary(true)); - myCodeSystemProvider = new IValidateCodeProvidersR4.MyCodeSystemProviderR4(); - myValueSetProvider = new IValidateCodeProvidersR4.MyValueSetProviderR4(); + myCodeSystemProvider = new IValidationProvidersR4.MyCodeSystemProviderR4(); + myValueSetProvider = new IValidationProvidersR4.MyValueSetProviderR4(); ourRestfulServerExtension.getRestfulServer().registerProviders(myCodeSystemProvider, myValueSetProvider); } @@ -95,12 +101,12 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa } @Override - public IValidationProviders.IMyCodeSystemProvider getCodeSystemProvider() { + public IValidationProviders.IMyValidationProvider getCodeSystemProvider() { return myCodeSystemProvider; } @Override - public IValidationProviders.IMyValueSetProvider getValueSetProvider() { + public IValidationProviders.IMyValidationProvider getValueSetProvider() { return myValueSetProvider; } @@ -116,51 +122,40 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @Override public IBaseOperationOutcome getCodeSystemInvalidCodeOutcome() { - return ClasspathUtil.loadResource(getService().getFhirContext(), OperationOutcome.class, "/terminology/OperationOutcome-CodeSystem-invalid-code.json"); + return getCodeSystemInvalidCodeOutcome(OperationOutcome.class); } @Override public IBaseOperationOutcome getValueSetInvalidCodeOutcome() { - return ClasspathUtil.loadResource(getService().getFhirContext(), OperationOutcome.class, "/terminology/OperationOutcome-ValueSet-invalid-code.json"); + return getValueSetInvalidCodeOutcome(OperationOutcome.class); } @Override - public List getCodeValidationIssues(IBaseOperationOutcome theOperationOutcome) { - return ((OperationOutcome)theOperationOutcome).getIssue().stream() - .map(issueComponent -> new IValidationSupport.CodeValidationIssue( - issueComponent.getDetails().getText(), - IValidationSupport.IssueSeverity.ERROR, - /* assume issue type is OperationOutcome.IssueType#CODEINVALID as it is the only match */ - IValidationSupport.CodeValidationIssueCode.INVALID, - IValidationSupport.CodeValidationIssueCoding.INVALID_CODE)) - .toList(); + public IBaseOperationOutcome getValueSetCustomDetailCodeOutcome() { + return getValueSetCustomDetailCodeOutcome(OperationOutcome.class); } @Test void validateCodeInValueSet_success() { - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); - CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, new ConceptValidationOptions(), IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, new ConceptValidationOptions(), CODE_SYSTEM, CODE, DISPLAY, valueSet); assertNotNull(outcome); - assertEquals(IValidationProviders.CODE, outcome.getCode()); - assertEquals(IValidationProviders.DISPLAY, outcome.getDisplay()); + assertEquals(CODE, outcome.getCode()); + assertEquals(DISPLAY, outcome.getDisplay()); assertNull(outcome.getSeverity()); assertNull(outcome.getMessage()); - - assertEquals(IValidationProviders.CODE, myValueSetProvider.getCode()); - assertEquals(IValidationProviders.DISPLAY, myValueSetProvider.getDisplay()); - assertEquals(IValidationProviders.VALUE_SET_URL, myValueSetProvider.getValueSet()); } @Override public Parameters createParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) { Parameters parameters = new Parameters() - .addParameter("code", IValidationProviders.CODE) - .addParameter("system", IValidationProviders.CODE_SYSTEM) - .addParameter("version", IValidationProviders.CODE_SYSTEM_VERSION) + .addParameter("code", CODE) + .addParameter("system", CODE_SYSTEM) + .addParameter("version", CODE_SYSTEM_VERSION) .addParameter("display", theDisplay) .addParameter("message", theMessage); if (theResult != null) { @@ -181,16 +176,16 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @Test void validateCodeInValueSet_uniqueComposeInclude() { - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); String systemUrl = "http://hl7.org/fhir/ValueSet/administrative-gender"; valueSet.setCompose(new ValueSet.ValueSetComposeComponent().setInclude( Collections.singletonList(new ValueSet.ConceptSetComponent().setSystem(systemUrl)) )); CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, - new ConceptValidationOptions().setInferSystem(true), null, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + new ConceptValidationOptions().setInferSystem(true), null, CODE, DISPLAY, valueSet); // validate service doesn't return error message (as when no code system is present) assertNotNull(outcome); @@ -211,16 +206,16 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @ParameterizedTest @MethodSource(value = "getRemoteTerminologyServerExceptions") void validateCodeInValueSet_systemNotPresent_returnsValidationResultWithError(Exception theException, String theServerMessage) { - myValueSetProvider.setException(theException); - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + getValueSetProvider().addException("$validate-code", VALUE_SET_URL, CODE, theException); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); valueSet.setCompose(new ValueSet.ValueSetComposeComponent().setInclude( Lists.newArrayList(new ValueSet.ConceptSetComponent(), new ValueSet.ConceptSetComponent()))); CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, - new ConceptValidationOptions().setInferSystem(true), null, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + new ConceptValidationOptions().setInferSystem(true), null, CODE, DISPLAY, valueSet); String unknownCodeForValueSetError = "Unknown code \"null#CODE\" for ValueSet with URL \"http://value.set/url\". The Remote Terminology server http://"; verifyErrorResultFromException(outcome, unknownCodeForValueSetError, theServerMessage); @@ -230,11 +225,11 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @ParameterizedTest @MethodSource(value = "getRemoteTerminologyServerExceptions") void validateCodeInValueSet_systemPresentCodeNotPresent_returnsValidationResultWithError(Exception theException, String theServerMessage) { - myValueSetProvider.setException(theException); - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + getValueSetProvider().addException(JpaConstants.OPERATION_VALIDATE_CODE, VALUE_SET_URL, CODE, theException); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); String systemUrl = "http://hl7.org/fhir/ValueSet/administrative-gender"; String systemUrl2 = "http://hl7.org/fhir/ValueSet/other-valueset"; valueSet.setCompose(new ValueSet.ValueSetComposeComponent().setInclude( @@ -243,7 +238,7 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa new ValueSet.ConceptSetComponent().setSystem(systemUrl2)))); CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, - new ConceptValidationOptions().setInferSystem(true), null, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + new ConceptValidationOptions().setInferSystem(true), null, CODE, DISPLAY, valueSet); String unknownCodeForValueSetError = "Unknown code \"null#CODE\" for ValueSet with URL \"http://value.set/url\". The Remote Terminology server http://"; verifyErrorResultFromException(outcome, unknownCodeForValueSetError, theServerMessage); @@ -252,10 +247,10 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @Test void validateCodeInValueSet_systemPresentCodePresentValidatesOKNoVersioned() { - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); String systemUrl = "http://hl7.org/fhir/ValueSet/administrative-gender"; String systemUrl2 = "http://hl7.org/fhir/ValueSet/other-valueset"; valueSet.setCompose(new ValueSet.ValueSetComposeComponent().setInclude( @@ -264,14 +259,14 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa new ValueSet.ConceptSetComponent().setSystem(systemUrl2).setConcept( Lists.newArrayList( new ValueSet.ConceptReferenceComponent().setCode("not-the-code"), - new ValueSet.ConceptReferenceComponent().setCode(IValidationProviders.CODE) ) + new ValueSet.ConceptReferenceComponent().setCode(CODE) ) )) )); TestClientInterceptor requestInterceptor = new TestClientInterceptor(); mySvc.addClientInterceptor(requestInterceptor); CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, - new ConceptValidationOptions().setInferSystem(true), null, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + new ConceptValidationOptions().setInferSystem(true), null, CODE, DISPLAY, valueSet); assertNotNull(outcome); assertEquals(systemUrl2, requestInterceptor.getCapturedSystemParameter()); @@ -280,10 +275,10 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @Test void validateCodeInValueSet_systemPresentCodePresentValidatesOKVersioned() { - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); String systemUrl = "http://hl7.org/fhir/ValueSet/administrative-gender"; String systemVersion = "3.0.2"; String systemUrl2 = "http://hl7.org/fhir/ValueSet/other-valueset"; @@ -294,14 +289,14 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa new ValueSet.ConceptSetComponent().setSystem(systemUrl2).setVersion(system2Version).setConcept( Lists.newArrayList( new ValueSet.ConceptReferenceComponent().setCode("not-the-code"), - new ValueSet.ConceptReferenceComponent().setCode(IValidationProviders.CODE) ) + new ValueSet.ConceptReferenceComponent().setCode(CODE) ) )) )); TestClientInterceptor requestInterceptor = new TestClientInterceptor(); mySvc.addClientInterceptor(requestInterceptor); CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, - new ConceptValidationOptions().setInferSystem(true), null, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + new ConceptValidationOptions().setInferSystem(true), null, CODE, DISPLAY, valueSet); assertNotNull(outcome); assertEquals(systemUrl2 + "|" + system2Version, requestInterceptor.getCapturedSystemParameter()); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java index ca55a41bf4c..d5035a8048e 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java @@ -31,6 +31,7 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4b.conformance.ProfileUtilities; import org.hl7.fhir.r4b.context.IWorkerContext; +import org.hl7.fhir.r4b.fhirpath.FHIRPathEngine; import org.hl7.fhir.r4b.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4b.model.AllergyIntolerance; import org.hl7.fhir.r4b.model.Base; @@ -61,7 +62,6 @@ import org.hl7.fhir.r4b.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.r4b.model.ValueSet; import org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionComponent; import org.hl7.fhir.r4b.terminologies.ValueSetExpander; -import org.hl7.fhir.r4b.fhirpath.FHIRPathEngine; import org.hl7.fhir.r5.test.utils.ClassesLoadedFlags; import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; @@ -203,7 +203,7 @@ public class FhirInstanceValidatorR4BTest extends BaseValidationTestWithInlineMo retVal = new IValidationSupport.CodeValidationResult().setCode(code); } else if (myValidSystems.contains(system)) { final String message = "Unknown code (for '" + system + "#" + code + "')"; - return 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))); + return 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 { retVal = myDefaultValidationSupport.validateCode(new ValidationSupportContext(myDefaultValidationSupport), options, system, code, display, valueSetUrl); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java index f0ea48686e1..abcb0f94704 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java @@ -48,7 +48,6 @@ import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; -import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; @@ -200,10 +199,10 @@ public class FhirInstanceValidatorR5Test extends BaseValidationTestWithInlineMoc retVal = new IValidationSupport.CodeValidationResult().setCode(code); } else if (myValidSystems.contains(system)) { String theMessage = "Unknown code (for '" + system + "#" + code + "')"; - retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(theMessage).setCodeValidationIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(theMessage, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE))); + retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(theMessage).setIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(theMessage, 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 { retVal = myDefaultValidationSupport.validateCode(new ValidationSupportContext(myDefaultValidationSupport), options, system, code, display, valueSetUrl); } diff --git a/hapi-fhir-validation/src/test/resources/terminology/OperationOutcome-ValueSet-custom-issue-detail.json b/hapi-fhir-validation/src/test/resources/terminology/OperationOutcome-ValueSet-custom-issue-detail.json new file mode 100644 index 00000000000..0823a430cf8 --- /dev/null +++ b/hapi-fhir-validation/src/test/resources/terminology/OperationOutcome-ValueSet-custom-issue-detail.json @@ -0,0 +1,22 @@ +{ + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "information", + "code": "processing", + "details": { + "coding": [ + { + "system": "http://example.com/custom-issue-type", + "code": "valueset-is-draft" + } + ] + }, + "diagnostics": "The ValueSet status is marked as draft.", + "location": [ + "Bundle", + "Line[1] Col[2]" + ] + } + ] +} \ No newline at end of file