Add terminology mapping and display population interceptors (#2488)

* Start work on code mapping interceptors

* Work on generation

* Add terminology mapping and display population interceptors

* Add changelog

* License header updates

* Test fixes

* Fix typo

* Interceptor fixes

* License header update

* Test fixes

* Mapping fixes

* Version bump

* License header updates

* Add null tests

* Add null guard

* Intermittent fix
This commit is contained in:
James Agnew 2021-03-21 12:03:57 -04:00 committed by GitHub
parent 009f184ab4
commit dc41d5ad61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 4282 additions and 3421 deletions

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -24,6 +24,8 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.ParametersUtil;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -84,6 +86,7 @@ public interface IValidationSupport {
* @param theValueSetToExpand The valueset that should be expanded
* @return The expansion, or null
*/
@Nullable
default ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, @Nullable ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) {
return null;
}
@ -93,6 +96,7 @@ public interface IValidationSupport {
* validation support module. This method may return null if it doesn't
* make sense for a given module.
*/
@Nullable
default List<IBaseResource> fetchAllConformanceResources() {
return null;
}
@ -100,6 +104,7 @@ public interface IValidationSupport {
/**
* Load and return all possible structure definitions
*/
@Nullable
default <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
return null;
}
@ -110,6 +115,7 @@ public interface IValidationSupport {
* @param theSystem The code system
* @return The valueset (must not be null, but can be an empty ValueSet)
*/
@Nullable
default IBaseResource fetchCodeSystem(String theSystem) {
return null;
}
@ -128,6 +134,7 @@ public interface IValidationSupport {
* given URI can be found
*/
@SuppressWarnings("unchecked")
@Nullable
default <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) {
Validate.notBlank(theUri, "theUri must not be null or blank");
@ -161,6 +168,7 @@ public interface IValidationSupport {
return null;
}
@Nullable
default IBaseResource fetchStructureDefinition(String theUrl) {
return null;
}
@ -182,6 +190,7 @@ public interface IValidationSupport {
/**
* Fetch the given ValueSet by URL
*/
@Nullable
default IBaseResource fetchValueSet(String theValueSetUrl) {
return null;
}
@ -199,6 +208,7 @@ public interface IValidationSupport {
* @param theDisplay The display name, if it should also be validated
* @return Returns a validation result object
*/
@Nullable
default CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
return null;
}
@ -216,6 +226,7 @@ public interface IValidationSupport {
* @param theValueSet The ValueSet to validate against. Must not be null, and must be a ValueSet resource.
* @return Returns a validation result object, or <code>null</code> if this validation support module can not handle this kind of request
*/
@Nullable
default CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
return null;
}
@ -228,6 +239,7 @@ public interface IValidationSupport {
* @param theSystem The CodeSystem URL
* @param theCode The code
*/
@Nullable
default LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) {
return null;
}
@ -251,6 +263,7 @@ public interface IValidationSupport {
* other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter.
* @return Returns null if this module does not know how to handle this request
*/
@Nullable
default IBaseResource generateSnapshot(ValidationSupportContext theValidationSupportContext, IBaseResource theInput, String theUrl, String theWebUrl, String theProfileName) {
return null;
}
@ -268,6 +281,14 @@ public interface IValidationSupport {
// nothing
}
/**
* Attempt to translate the given concept from one code system to another
*/
@Nullable
default TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
return null;
}
enum IssueSeverity {
/**
@ -289,6 +310,7 @@ public interface IValidationSupport {
}
class ConceptDesignation {
private String myLanguage;
private String myUseSystem;
private String myUseCode;
@ -710,4 +732,62 @@ public interface IValidationSupport {
}
}
class TranslateCodeRequest {
private final String mySourceSystemUrl;
private final String mySourceCode;
private final String myTargetSystemUrl;
private final int myHashCode;
public TranslateCodeRequest(String theSourceSystemUrl, String theSourceCode, String theTargetSystemUrl) {
mySourceSystemUrl = theSourceSystemUrl;
mySourceCode = theSourceCode;
myTargetSystemUrl = theTargetSystemUrl;
myHashCode = new HashCodeBuilder(17, 37)
.append(mySourceSystemUrl)
.append(mySourceCode)
.append(myTargetSystemUrl)
.toHashCode();
}
@Override
public boolean equals(Object theO) {
if (this == theO) {
return true;
}
if (theO == null || getClass() != theO.getClass()) {
return false;
}
TranslateCodeRequest that = (TranslateCodeRequest) theO;
return new EqualsBuilder()
.append(mySourceSystemUrl, that.mySourceSystemUrl)
.append(mySourceCode, that.mySourceCode)
.append(myTargetSystemUrl, that.myTargetSystemUrl)
.isEquals();
}
@Override
public int hashCode() {
return myHashCode;
}
public String getSourceSystemUrl() {
return mySourceSystemUrl;
}
public String getSourceCode() {
return mySourceCode;
}
public String getTargetSystemUrl() {
return myTargetSystemUrl;
}
}
}

View File

@ -0,0 +1,154 @@
package ca.uhn.fhir.context.support;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2021 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%
*/
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class TranslateConceptResult {
private String mySystem;
private String myCode;
private String myDisplay;
private String myEquivalence;
private String myConceptMapUrl;
private String myValueSet;
private String mySystemVersion;
/**
* Constructor
*/
public TranslateConceptResult() {
super();
}
public String getSystem() {
return mySystem;
}
public TranslateConceptResult setSystem(String theSystem) {
mySystem = theSystem;
return this;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("system", mySystem)
.append("code", myCode)
.append("display", myDisplay)
.append("equivalence", myEquivalence)
.append("conceptMapUrl", myConceptMapUrl)
.append("valueSet", myValueSet)
.append("systemVersion", mySystemVersion)
.toString();
}
public String getCode() {
return myCode;
}
public TranslateConceptResult setCode(String theCode) {
myCode = theCode;
return this;
}
public String getDisplay() {
return myDisplay;
}
public TranslateConceptResult setDisplay(String theDisplay) {
myDisplay = theDisplay;
return this;
}
public String getEquivalence() {
return myEquivalence;
}
public TranslateConceptResult setEquivalence(String theEquivalence) {
myEquivalence = theEquivalence;
return this;
}
public String getSystemVersion() {
return mySystemVersion;
}
public void setSystemVersion(String theSystemVersion) {
mySystemVersion = theSystemVersion;
}
public String getValueSet() {
return myValueSet;
}
public TranslateConceptResult setValueSet(String theValueSet) {
myValueSet = theValueSet;
return this;
}
public String getConceptMapUrl() {
return myConceptMapUrl;
}
public TranslateConceptResult setConceptMapUrl(String theConceptMapUrl) {
myConceptMapUrl = theConceptMapUrl;
return this;
}
@Override
public boolean equals(Object theO) {
if (this == theO) {
return true;
}
if (theO == null || getClass() != theO.getClass()) {
return false;
}
TranslateConceptResult that = (TranslateConceptResult) theO;
EqualsBuilder b = new EqualsBuilder();
b.append(mySystem, that.mySystem);
b.append(myCode, that.myCode);
b.append(myDisplay, that.myDisplay);
b.append(myEquivalence, that.myEquivalence);
b.append(myConceptMapUrl, that.myConceptMapUrl);
b.append(myValueSet, that.myValueSet);
b.append(mySystemVersion, that.mySystemVersion);
return b.isEquals();
}
@Override
public int hashCode() {
HashCodeBuilder b = new HashCodeBuilder(17, 37);
b.append(mySystem);
b.append(myCode);
b.append(myDisplay);
b.append(myEquivalence);
b.append(myConceptMapUrl);
b.append(myValueSet);
b.append(mySystemVersion);
return b.toHashCode();
}
}

View File

@ -0,0 +1,94 @@
package ca.uhn.fhir.context.support;
/*
* #%L
* HAPI FHIR - Core Library
* %%
* Copyright (C) 2014 - 2021 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%
*/
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import java.util.ArrayList;
import java.util.List;
public class TranslateConceptResults {
private List<TranslateConceptResult> myResults;
private String myMessage;
private boolean myResult;
public TranslateConceptResults() {
super();
myResults = new ArrayList<>();
}
public List<TranslateConceptResult> getResults() {
return myResults;
}
public void setResults(List<TranslateConceptResult> theResults) {
myResults = theResults;
}
public String getMessage() {
return myMessage;
}
public void setMessage(String theMessage) {
myMessage = theMessage;
}
public boolean getResult() {
return myResult;
}
public void setResult(boolean theMatched) {
myResult = theMatched;
}
public int size() {
return getResults().size();
}
public boolean isEmpty() {
return getResults().isEmpty();
}
@Override
public boolean equals(Object theO) {
if (this == theO) {
return true;
}
if (theO == null || getClass() != theO.getClass()) {
return false;
}
TranslateConceptResults that = (TranslateConceptResults) theO;
EqualsBuilder b = new EqualsBuilder();
b.append(myResults, that.myResults);
return b.isEquals();
}
@Override
public int hashCode() {
HashCodeBuilder b = new HashCodeBuilder(17, 37);
b.append(myResults);
return b.toHashCode();
}
}

View File

@ -32,7 +32,7 @@ import java.util.List;
* THIS API IS EXPERIMENTAL IN HAPI FHIR - USE WITH CAUTION AS THE PUBLISHED API MAY
* CHANGE
*
* @see FhirTerser#visit(IBaseResource, IModelVisitor2)
* @see FhirTerser#visit(IBase, IModelVisitor2)
*/
public interface IModelVisitor2 {

View File

@ -131,12 +131,9 @@ ca.uhn.fhir.jpa.dao.LegacySearchBuilder.sourceParamDisabled=The _source paramete
ca.uhn.fhir.jpa.dao.LegacySearchBuilder.invalidCodeMissingSystem=Invalid token specified for parameter {0} - No system specified: {1}|{2}
ca.uhn.fhir.jpa.dao.LegacySearchBuilder.invalidCodeMissingCode=Invalid token specified for parameter {0} - No code specified: {1}|{2}
ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.matchesFound=Matches found!
ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.noMatchesFound=No matches found!
ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoConceptMapR4.matchesFound=Matches found!
ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoConceptMapR4.noMatchesFound=No matches found!
ca.uhn.fhir.jpa.dao.r5.FhirResourceDaoConceptMapR5.matchesFound=Matches found!
ca.uhn.fhir.jpa.dao.r5.FhirResourceDaoConceptMapR5.noMatchesFound=No matches found!
ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl.matchesFound=Matches found
ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl.noMatchesFound=No Matches found
ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoSearchParameterR4.invalidSearchParamExpression=The expression "{0}" can not be evaluated and may be invalid: {1}
ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderToken.textModifierDisabledForSearchParam=The :text modifier is disabled for this search parameter

View File

@ -3,14 +3,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -78,13 +78,13 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-subscription</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
@ -101,7 +101,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-testpage-overlay</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<classifier>classes</classifier>
</dependency>
<dependency>

View File

@ -21,6 +21,7 @@ package ca.uhn.hapi.fhir.docs;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.interceptor.*;
@ -223,4 +224,25 @@ public class ServletExamples {
}
// END SNIPPET: corsInterceptor
@WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server")
public class RestfulServerWithResponseTerminologyTranslationInterceptor extends RestfulServer {
private IValidationSupport myValidationSupport;
@Override
protected void initialize() throws ServletException {
// START SNIPPET: ResponseTerminologyTranslationInterceptor
// Create an interceptor that will map from a proprietary CodeSystem to LOINC
ResponseTerminologyTranslationInterceptor interceptor = new ResponseTerminologyTranslationInterceptor(myValidationSupport);
interceptor.addMappingSpecification("http://examplelabs.org", "http://loinc.org");
// Register the interceptor
registerInterceptor(interceptor);
// END SNIPPET: ResponseTerminologyTranslationInterceptor
}
}
}

View File

@ -0,0 +1,5 @@
---
type: add
issue: 2488
title: "Two new server interceptors have been added that can be used to map codes and populate code display names respectively
using the server terminology services."

View File

@ -193,6 +193,54 @@ If you wish to override the value of `Resource.meta.source` using the value supp
* [CaptureResourceSourceFromHeaderInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/CaptureResourceSourceFromHeaderInterceptor.html)
* [CaptureResourceSourceFromHeaderInterceptor Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/CaptureResourceSourceFromHeaderInterceptor.java)
# Terminology: Map Response Terminology
A common problem when implementing FHIR APIs is the challenge of how to return coded data using standard vocabularies when your source data is not modelled using these vocabularies. For example, suppose you want to implement support for an Implementation Guide that mandates the use of [LOINC](https://loinc.org) but your source data uses local/proprietary observation codes.
One solution is to simply apply mappings and add them to the FHIR data you are storing in your repository as you are storing it. This solution, often called *Mapping on the Way In*, will work but it has potential pitfalls including:
* All mappings must be known at the time the data is being stored.
* If mappings change because of mistakes or new information, updating existing data is difficult.
A potentially better solution is to apply *Mapping on the Way Out*, meaning that your mappings are stored in a central spot and applied at runtime to data as it is leaving your system. HAPI FHIR supplies an interceptor called the ResponseTerminologyTranslationInterceptor that can help with this.
* [ResponseTerminologyTranslationInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/ResponseTerminologyTranslationInterceptor.html)
* [ResponseTerminologyTranslationInterceptor Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseTerminologyTranslationInterceptor.java)
This interceptor uses ConceptMap resources that are stored in your system, looking up mappings for CodeableConcept codings in your resources and adding them to the responses.
The following code snippet shows a simple example of how to create and configure this interceptor.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ServletExamples.java|ResponseTerminologyTranslationInterceptor}}
```
## Limitations
The following limitations will hopefully be resolved in the future:
This interceptor currently only works when registered against a RestfulServer backed by the HAPI FHIR JPA server.
This interceptor only modifies responses to FHIR read/vread/search/history operations. Responses to these operations are not modified if they are found within a FHIR transaction operation.
# Terminology: Populate Code Display Names
The HAPI FHIR ResponseTerminologyDisplayPopulationInterceptor interceptor looks for Coding elements within responses where the `Coding.system` and `Coding.code` values are populated but the `Coding.display` is not. The interceptor will attempt to resolve the correct display using the validation support module and will add it to the Coding display value if one is found.
* [ResponseTerminologyDisplayPopulationInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/ResponseTerminologyDisplayPopulationInterceptor.html)
* [ResponseTerminologyDisplayPopulationInterceptor Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseTerminologyDisplayPopulationInterceptor.java)
This interceptor uses ConceptMap resources that are stored in your system, looking up mappings for CodeableConcept codings in your resources and adding them to the responses.
## Limitations
The following limitation will hopefully be resolved in the future:
This interceptor only modifies responses to FHIR read/vread/search/history operations. Responses to these operations are not modified if they are found within a FHIR transaction operation.
# Utility: ResponseSizeCapturingInterceptor
The ResponseSizeCapturingInterceptor can be used to capture the number of characters written in each HTTP FHIR response.
@ -229,6 +277,8 @@ The UserRequestRetryVersionConflictsInterceptor allows clients to request that t
The RepositoryValidatingInterceptor can be used to enforce validation rules on data stored in a HAPI FHIR JPA Repository. See [Repository Validating Interceptor](/docs/validation/repository_validating_interceptor.html) for more information.
# Data Standardization
```StandardizingInterceptor``` handles data standardization (s13n) requirements. This interceptor applies standardization rules on all FHIR primitives as configured in the ```s13n.json``` file that should be made available on the classpath. This file contains FHIRPath definitions together with the standardizers that should be applied to that path. It comes with six per-build standardizers: NAME_FAMILY, NAME_GIVEN, EMAIL, TITLE, PHONE and TEXT. Custom standardizers can be developed by implementing ```ca.uhn.fhir.rest.server.interceptor.s13n.standardizers.IStandardizer``` interface.

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -14,6 +14,7 @@ import org.hl7.fhir.r4.model.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -93,6 +94,7 @@ public class DaoConfig {
/**
* update setter javadoc if default changes
*/
@Nonnull
private Long myTranslationCachesExpireAfterWriteInMinutes = DEFAULT_TRANSLATION_CACHES_EXPIRE_AFTER_WRITE_IN_MINUTES;
/**
* update setter javadoc if default changes
@ -884,6 +886,7 @@ public class DaoConfig {
* Specifies the duration in minutes for which values will be retained after being
* written to the terminology translation cache. Defaults to 60.
*/
@Nonnull
public Long getTranslationCachesExpireAfterWriteInMinutes() {
return myTranslationCachesExpireAfterWriteInMinutes;
}

View File

@ -21,10 +21,10 @@ package ca.uhn.fhir.jpa.api.dao;
*/
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.jpa.api.model.TranslationResult;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface IFhirResourceDaoConceptMap<T extends IBaseResource> extends IFhirResourceDao<T> {
TranslationResult translate(TranslationRequest theTranslationRequest, RequestDetails theRequestDetails);
TranslateConceptResults translate(TranslationRequest theTranslationRequest, RequestDetails theRequestDetails);
}

View File

@ -1,74 +0,0 @@
package ca.uhn.fhir.jpa.api.model;
/*
* #%L
* HAPI FHIR JPA API
* %%
* Copyright (C) 2014 - 2021 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%
*/
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.UriType;
public class TranslationMatch {
private Coding myConcept;
private CodeType myEquivalence;
private UriType mySource;
public TranslationMatch() {
super();
}
public Coding getConcept() {
return myConcept;
}
public void setConcept(Coding theConcept) {
myConcept = theConcept;
}
public CodeType getEquivalence() {
return myEquivalence;
}
public void setEquivalence(CodeType theEquivalence) {
myEquivalence = theEquivalence;
}
public UriType getSource() {
return mySource;
}
public void setSource(UriType theSource) {
mySource = theSource;
}
public void toParameterParts(ParametersParameterComponent theParam) {
if (myEquivalence != null) {
theParam.addPart().setName("equivalence").setValue(myEquivalence);
}
if (myConcept != null) {
theParam.addPart().setName("concept").setValue(myConcept);
}
if (mySource != null) {
theParam.addPart().setName("source").setValue(mySource);
}
}
}

View File

@ -1,88 +0,0 @@
package ca.uhn.fhir.jpa.api.model;
/*
* #%L
* HAPI FHIR JPA API
* %%
* Copyright (C) 2014 - 2021 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%
*/
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.StringType;
import java.util.ArrayList;
import java.util.List;
public class TranslationResult {
private List<TranslationMatch> myMatches;
private StringType myMessage;
private BooleanType myResult;
public TranslationResult() {
super();
myMatches = new ArrayList<>();
}
public List<TranslationMatch> getMatches() {
return myMatches;
}
public void setMatches(List<TranslationMatch> theMatches) {
myMatches = theMatches;
}
public boolean addMatch(TranslationMatch theMatch) {
return myMatches.add(theMatch);
}
public StringType getMessage() {
return myMessage;
}
public void setMessage(StringType theMessage) {
myMessage = theMessage;
}
public BooleanType getResult() {
return myResult;
}
public void setResult(BooleanType theMatched) {
myResult = theMatched;
}
public Parameters toParameters() {
Parameters retVal = new Parameters();
if (myResult != null) {
retVal.addParameter().setName("result").setValue(myResult);
}
if (myMessage != null) {
retVal.addParameter().setName("message").setValue(myMessage);
}
for (TranslationMatch translationMatch : myMatches) {
ParametersParameterComponent matchParam = retVal.addParameter().setName("match");
translationMatch.toParameterParts(matchParam);
}
return retVal;
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
@ -60,6 +61,7 @@ import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationInterceptor;
import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager;
@ -119,6 +121,8 @@ import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.sp.SearchParamPresenceSvcImpl;
import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.jpa.validation.JpaResourceLoader;
import ca.uhn.fhir.jpa.validation.ValidationSettings;
@ -251,6 +255,13 @@ public abstract class BaseConfig {
return new CascadingDeleteInterceptor(theFhirContext, theDaoRegistry, theInterceptorBroadcaster);
}
@Lazy
@Bean
public ResponseTerminologyTranslationInterceptor responseTerminologyTranslationInterceptor(IValidationSupport theValidationSupport) {
return new ResponseTerminologyTranslationInterceptor(theValidationSupport);
}
/**
* This method should be overridden to provide an actual completed
* bean, but it provides a partially completed entity manager
@ -336,6 +347,11 @@ public abstract class BaseConfig {
return new DatabaseSearchResultCacheSvcImpl();
}
@Bean
public ITermConceptMappingSvc termConceptMappingSvc() {
return new TermConceptMappingSvcImpl();
}
@Bean
public ThreadPoolTaskExecutor searchCoordinatorThreadFactory() {
final ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();

View File

@ -82,7 +82,12 @@ public abstract class BaseConfigDstu3Plus extends BaseConfig {
@Primary
@Bean
public IValidationSupport validationSupportChain() {
return new CachingValidationSupport(jpaValidationSupportChain());
// Short timeout for code translation because TermConceptMappingSvcImpl has its own caching
CachingValidationSupport.CacheTimeouts cacheTimeouts = CachingValidationSupport.CacheTimeouts.defaultValues()
.setTranslateCodeMillis(1000);
return new CachingValidationSupport(jpaValidationSupportChain(), cacheTimeouts);
}
@Bean(name = "myInstanceValidator")

View File

@ -21,147 +21,38 @@ package ca.uhn.fhir.jpa.dao.dstu3;
*/
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoConceptMap;
import ca.uhn.fhir.jpa.api.model.TranslationMatch;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.jpa.api.model.TranslationResult;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.dstu3.model.ConceptMap;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.UriType;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.hl7.fhir.convertors.conv30_40.ConceptMap30_40.convertConceptMap;
public class FhirResourceDaoConceptMapDstu3 extends BaseHapiFhirResourceDao<ConceptMap> implements IFhirResourceDaoConceptMap<ConceptMap> {
@Autowired
private ITermReadSvc myHapiTerminologySvc;
private ITermConceptMappingSvc myTermConceptMappingSvc;
@Override
public TranslationResult translate(TranslationRequest theTranslationRequest, RequestDetails theRequestDetails) {
public TranslateConceptResults translate(TranslationRequest theTranslationRequest, RequestDetails theRequestDetails) {
if (theTranslationRequest.hasReverse() && theTranslationRequest.getReverseAsBoolean()) {
return buildReverseTranslationResult(myHapiTerminologySvc.translateWithReverse(theTranslationRequest));
return myTermConceptMappingSvc.translateWithReverse(theTranslationRequest);
}
return buildTranslationResult(myHapiTerminologySvc.translate(theTranslationRequest));
return myTermConceptMappingSvc.translate(theTranslationRequest);
}
private TranslationResult buildTranslationResult(List<TermConceptMapGroupElementTarget> theTargets) {
TranslationResult retVal = new TranslationResult();
String msg;
if (theTargets.isEmpty()) {
retVal.setResult(new BooleanType(false));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapDstu3.class,
"noMatchesFound");
retVal.setMessage(new StringType(msg));
} else {
retVal.setResult(new BooleanType(true));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapDstu3.class,
"matchesFound");
retVal.setMessage(new StringType(msg));
TranslationMatch translationMatch;
Set<TermConceptMapGroupElementTarget> targetsToReturn = new HashSet<>();
for (TermConceptMapGroupElementTarget target : theTargets) {
if (targetsToReturn.add(target)) {
translationMatch = new TranslationMatch();
if (target.getEquivalence() != null) {
translationMatch.setEquivalence(new CodeType(target.getEquivalence().toCode()));
}
translationMatch.setConcept(
new Coding()
.setCode(target.getCode())
.setSystem(target.getSystem())
.setVersion(target.getSystemVersion())
.setDisplay(target.getDisplay())
);
translationMatch.setSource(new UriType(target.getConceptMapUrl()));
retVal.addMatch(translationMatch);
}
}
}
return retVal;
}
private TranslationResult buildReverseTranslationResult(List<TermConceptMapGroupElement> theElements) {
TranslationResult retVal = new TranslationResult();
String msg;
if (theElements.isEmpty()) {
retVal.setResult(new BooleanType(false));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapDstu3.class,
"noMatchesFound");
retVal.setMessage(new StringType(msg));
} else {
retVal.setResult(new BooleanType(true));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapDstu3.class,
"matchesFound");
retVal.setMessage(new StringType(msg));
TranslationMatch translationMatch;
Set<TermConceptMapGroupElement> elementsToReturn = new HashSet<>();
for (TermConceptMapGroupElement element : theElements) {
if (elementsToReturn.add(element)) {
translationMatch = new TranslationMatch();
translationMatch.setConcept(
new Coding()
.setCode(element.getCode())
.setSystem(element.getSystem())
.setVersion(element.getSystemVersion())
.setDisplay(element.getDisplay())
);
translationMatch.setSource(new UriType(element.getConceptMapUrl()));
retVal.addMatch(translationMatch);
}
}
}
return retVal;
}
@Override
public ResourceTable updateEntity(RequestDetails theRequestDetails, IBaseResource theResource, IBasePersistedResource theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing,
@ -173,12 +64,12 @@ public class FhirResourceDaoConceptMapDstu3 extends BaseHapiFhirResourceDao<Conc
try {
ConceptMap conceptMap = (ConceptMap) theResource;
org.hl7.fhir.r4.model.ConceptMap converted = convertConceptMap(conceptMap);
myHapiTerminologySvc.storeTermConceptMapAndChildren(retVal, converted);
myTermConceptMappingSvc.storeTermConceptMapAndChildren(retVal, converted);
} catch (FHIRException fe) {
throw new InternalErrorException(fe);
}
} else {
myHapiTerminologySvc.deleteConceptMapAndChildren(retVal);
myTermConceptMappingSvc.deleteConceptMapAndChildren(retVal);
}
}

View File

@ -21,153 +21,31 @@ package ca.uhn.fhir.jpa.dao.r4;
*/
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoConceptMap;
import ca.uhn.fhir.jpa.api.model.TranslationMatch;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.jpa.api.model.TranslationResult;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.convertors.VersionConvertor_40_50;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.ConceptMap;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.UriType;
import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class FhirResourceDaoConceptMapR4 extends BaseHapiFhirResourceDao<ConceptMap> implements IFhirResourceDaoConceptMap<ConceptMap> {
@Autowired
private ITermReadSvc myHapiTerminologySvc;
private ITermConceptMappingSvc myTermConceptMappingSvc;
@Override
public TranslationResult translate(TranslationRequest theTranslationRequest, RequestDetails theRequestDetails) {
public TranslateConceptResults translate(TranslationRequest theTranslationRequest, RequestDetails theRequestDetails) {
if (theTranslationRequest.hasReverse() && theTranslationRequest.getReverseAsBoolean()) {
return buildReverseTranslationResult(myHapiTerminologySvc.translateWithReverse(theTranslationRequest));
return myTermConceptMappingSvc.translateWithReverse(theTranslationRequest);
}
return buildTranslationResult(myHapiTerminologySvc.translate(theTranslationRequest));
}
private TranslationResult buildTranslationResult(List<TermConceptMapGroupElementTarget> theTargets) {
TranslationResult retVal = new TranslationResult();
String msg;
if (theTargets.isEmpty()) {
retVal.setResult(new BooleanType(false));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapR4.class,
"noMatchesFound");
retVal.setMessage(new StringType(msg));
} else {
retVal.setResult(new BooleanType(true));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapR4.class,
"matchesFound");
retVal.setMessage(new StringType(msg));
TranslationMatch translationMatch;
Set<TermConceptMapGroupElementTarget> targetsToReturn = new HashSet<>();
for (TermConceptMapGroupElementTarget target : theTargets) {
if (targetsToReturn.add(target)) {
translationMatch = new TranslationMatch();
if (target.getEquivalence() != null) {
translationMatch.setEquivalence(new CodeType(target.getEquivalence().toCode()));
}
translationMatch.setConcept(
new Coding()
.setCode(target.getCode())
.setSystem(target.getSystem())
.setVersion(target.getSystemVersion())
.setDisplay(target.getDisplay())
);
translationMatch.setSource(new UriType(target.getConceptMapUrl()));
retVal.addMatch(translationMatch);
}
}
}
return retVal;
}
private TranslationResult buildReverseTranslationResult(List<TermConceptMapGroupElement> theElements) {
TranslationResult retVal = new TranslationResult();
String msg;
if (theElements.isEmpty()) {
retVal.setResult(new BooleanType(false));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapR4.class,
"noMatchesFound");
retVal.setMessage(new StringType(msg));
} else {
retVal.setResult(new BooleanType(true));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapR4.class,
"matchesFound");
retVal.setMessage(new StringType(msg));
TranslationMatch translationMatch;
Set<TermConceptMapGroupElement> elementsToReturn = new HashSet<>();
for (TermConceptMapGroupElement element : theElements) {
if (elementsToReturn.add(element)) {
translationMatch = new TranslationMatch();
translationMatch.setConcept(
new Coding()
.setCode(element.getCode())
.setSystem(element.getSystem())
.setVersion(element.getSystemVersion())
.setDisplay(element.getDisplay())
);
translationMatch.setSource(new UriType(element.getConceptMapUrl()));
if (element.getConceptMapGroupElementTargets().size() == 1) {
ConceptMapEquivalence eq = element.getConceptMapGroupElementTargets().get(0).getEquivalence();
if (eq != null) {
translationMatch.setEquivalence(new CodeType(eq.toCode()));
}
}
retVal.addMatch(translationMatch);
}
}
}
return retVal;
return myTermConceptMappingSvc.translate(theTranslationRequest);
}
@Override
@ -178,9 +56,9 @@ public class FhirResourceDaoConceptMapR4 extends BaseHapiFhirResourceDao<Concept
if (!retVal.isUnchangedInCurrentOperation()) {
if (retVal.getDeleted() == null) {
ConceptMap conceptMap = (ConceptMap) theResource;
myHapiTerminologySvc.storeTermConceptMapAndChildren(retVal, conceptMap);
myTermConceptMappingSvc.storeTermConceptMapAndChildren(retVal, conceptMap);
} else {
myHapiTerminologySvc.deleteConceptMapAndChildren(retVal);
myTermConceptMappingSvc.deleteConceptMapAndChildren(retVal);
}
}

View File

@ -1,22 +1,6 @@
package ca.uhn.fhir.jpa.dao.r5;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.hl7.fhir.convertors.VersionConvertor_40_50;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence;
import org.hl7.fhir.r5.model.BooleanType;
import org.hl7.fhir.r5.model.CodeType;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.ConceptMap;
import org.hl7.fhir.r5.model.StringType;
import org.hl7.fhir.r5.model.UriType;
import org.springframework.beans.factory.annotation.Autowired;
/*
/*-
* #%L
* HAPI FHIR JPA Server
* %%
@ -37,137 +21,31 @@ import org.springframework.beans.factory.annotation.Autowired;
*/
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoConceptMap;
import ca.uhn.fhir.jpa.api.model.TranslationMatch;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.jpa.api.model.TranslationResult;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget;
import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r5.model.ConceptMap;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
public class FhirResourceDaoConceptMapR5 extends BaseHapiFhirResourceDao<ConceptMap> implements IFhirResourceDaoConceptMap<ConceptMap> {
@Autowired
private ITermReadSvc myHapiTerminologySvc;
private ITermConceptMappingSvc myTermConceptMappingSvc;
@Override
public TranslationResult translate(TranslationRequest theTranslationRequest, RequestDetails theRequestDetails) {
public TranslateConceptResults translate(TranslationRequest theTranslationRequest, RequestDetails theRequestDetails) {
if (theTranslationRequest.hasReverse() && theTranslationRequest.getReverseAsBoolean()) {
return buildReverseTranslationResult(myHapiTerminologySvc.translateWithReverse(theTranslationRequest));
return myTermConceptMappingSvc.translateWithReverse(theTranslationRequest);
}
return buildTranslationResult(myHapiTerminologySvc.translate(theTranslationRequest));
}
private TranslationResult buildTranslationResult(List<TermConceptMapGroupElementTarget> theTargets) {
TranslationResult retVal = new TranslationResult();
String msg;
if (theTargets.isEmpty()) {
retVal.setResult(VersionConvertor_40_50.convertBoolean(new BooleanType(false)));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapR5.class,
"noMatchesFound");
retVal.setMessage(VersionConvertor_40_50.convertString(new StringType(msg)));
} else {
retVal.setResult(VersionConvertor_40_50.convertBoolean(new BooleanType(true)));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapR5.class,
"matchesFound");
retVal.setMessage(VersionConvertor_40_50.convertString(new StringType(msg)));
TranslationMatch translationMatch;
Set<TermConceptMapGroupElementTarget> targetsToReturn = new HashSet<>();
for (TermConceptMapGroupElementTarget target : theTargets) {
if (targetsToReturn.add(target)) {
translationMatch = new TranslationMatch();
if (target.getEquivalence() != null) {
translationMatch.setEquivalence(VersionConvertor_40_50.convertCode(new CodeType(target.getEquivalence().toCode())));
}
translationMatch.setConcept(VersionConvertor_40_50.convertCoding(
new Coding()
.setCode(target.getCode())
.setSystem(target.getSystem())
.setVersion(target.getSystemVersion())
.setDisplay(target.getDisplay())
));
translationMatch.setSource(VersionConvertor_40_50.convertUri(new UriType(target.getConceptMapUrl())));
retVal.addMatch(translationMatch);
}
}
}
return retVal;
}
private TranslationResult buildReverseTranslationResult(List<TermConceptMapGroupElement> theElements) {
TranslationResult retVal = new TranslationResult();
String msg;
if (theElements.isEmpty()) {
retVal.setResult(VersionConvertor_40_50.convertBoolean(new BooleanType(false)));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapR5.class,
"noMatchesFound");
retVal.setMessage(VersionConvertor_40_50.convertString(new StringType(msg)));
} else {
retVal.setResult(VersionConvertor_40_50.convertBoolean(new BooleanType(true)));
msg = getContext().getLocalizer().getMessage(
FhirResourceDaoConceptMapR5.class,
"matchesFound");
retVal.setMessage(VersionConvertor_40_50.convertString(new StringType(msg)));
TranslationMatch translationMatch;
Set<TermConceptMapGroupElement> elementsToReturn = new HashSet<>();
for (TermConceptMapGroupElement element : theElements) {
if (elementsToReturn.add(element)) {
translationMatch = new TranslationMatch();
translationMatch.setConcept(VersionConvertor_40_50.convertCoding(
new Coding()
.setCode(element.getCode())
.setSystem(element.getSystem())
.setVersion(element.getSystemVersion())
.setDisplay(element.getDisplay())
));
translationMatch.setSource(VersionConvertor_40_50.convertUri(new UriType(element.getConceptMapUrl())));
if (element.getConceptMapGroupElementTargets().size() == 1) {
ConceptMapEquivalence eq = element.getConceptMapGroupElementTargets().get(0).getEquivalence();
if (eq != null) {
translationMatch.setEquivalence(VersionConvertor_40_50.convertCode(new CodeType(eq.toCode())));
}
}
retVal.addMatch(translationMatch);
}
}
}
return retVal;
return myTermConceptMappingSvc.translate(theTranslationRequest);
}
@Override
@ -178,9 +56,9 @@ public class FhirResourceDaoConceptMapR5 extends BaseHapiFhirResourceDao<Concept
if (retVal.getDeleted() == null) {
ConceptMap conceptMap = (ConceptMap) theResource;
myHapiTerminologySvc.storeTermConceptMapAndChildren(retVal, org.hl7.fhir.convertors.conv40_50.ConceptMap40_50.convertConceptMap(conceptMap));
myTermConceptMappingSvc.storeTermConceptMapAndChildren(retVal, org.hl7.fhir.convertors.conv40_50.ConceptMap40_50.convertConceptMap(conceptMap));
} else {
myHapiTerminologySvc.deleteConceptMapAndChildren(retVal);
myTermConceptMappingSvc.deleteConceptMapAndChildren(retVal);
}
}

View File

@ -43,6 +43,13 @@ public class TermConceptMap implements Serializable {
static final int MAX_URL_LENGTH = 200;
public static final int MAX_VER_LENGTH = 200;
/**
* Constructor
*/
public TermConceptMap() {
super();
}
@Id()
@SequenceGenerator(name = "SEQ_CONCEPT_MAP_PID", sequenceName = "SEQ_CONCEPT_MAP_PID")
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_CONCEPT_MAP_PID")

View File

@ -22,8 +22,9 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoConceptMap;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.jpa.api.model.TranslationResult;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
@ -151,10 +152,10 @@ public class BaseJpaResourceProviderConceptMapDstu3 extends JpaResourceProviderD
startRequest(theServletRequest);
try {
IFhirResourceDaoConceptMap<ConceptMap> dao = (IFhirResourceDaoConceptMap<ConceptMap>) getDao();
TranslationResult result = dao.translate(translationRequest, theRequestDetails);
TranslateConceptResults result = dao.translate(translationRequest, theRequestDetails);
// Convert from R4 to DSTU3
return convertParameters(result.toParameters());
return convertParameters(TermConceptMappingSvcImpl.toParameters(result));
} catch (FHIRException fe) {
throw new InternalErrorException(fe);
} finally {

View File

@ -22,8 +22,9 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoConceptMap;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.jpa.api.model.TranslationResult;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
@ -141,8 +142,8 @@ public class BaseJpaResourceProviderConceptMapR4 extends JpaResourceProviderR4<C
startRequest(theServletRequest);
try {
IFhirResourceDaoConceptMap<ConceptMap> dao = (IFhirResourceDaoConceptMap<ConceptMap>) getDao();
TranslationResult result = dao.translate(translationRequest, theRequestDetails);
return result.toParameters();
TranslateConceptResults result = dao.translate(translationRequest, theRequestDetails);
return TermConceptMappingSvcImpl.toParameters(result);
} finally {
endRequest(theServletRequest);
}

View File

@ -22,8 +22,9 @@ package ca.uhn.fhir.jpa.provider.r5;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoConceptMap;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.jpa.api.model.TranslationResult;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
@ -142,8 +143,8 @@ public class BaseJpaResourceProviderConceptMapR5 extends JpaResourceProviderR5<C
startRequest(theServletRequest);
try {
IFhirResourceDaoConceptMap<ConceptMap> dao = (IFhirResourceDaoConceptMap<ConceptMap>) getDao();
TranslationResult result = dao.translate(translationRequest, theRequestDetails);
org.hl7.fhir.r4.model.Parameters parameters = result.toParameters();
TranslateConceptResults result = dao.translate(translationRequest, theRequestDetails);
org.hl7.fhir.r4.model.Parameters parameters = TermConceptMappingSvcImpl.toParameters(result);
return org.hl7.fhir.convertors.conv40_50.Parameters40_50.convertParameters(parameters);
} finally {
endRequest(theServletRequest);

View File

@ -29,18 +29,12 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IDao;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem;
import ca.uhn.fhir.jpa.api.model.TranslationQuery;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao;
import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDesignationDao;
@ -51,10 +45,6 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
import ca.uhn.fhir.jpa.entity.TermConceptMap;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroup;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
import ca.uhn.fhir.jpa.entity.TermConceptProperty;
@ -72,12 +62,12 @@ import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.jpa.term.ex.ExpansionTooCostlyException;
import ca.uhn.fhir.jpa.util.LogicUtil;
import ca.uhn.fhir.jpa.util.ScrollableResultsIterator;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -102,8 +92,6 @@ import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.RegexpQuery;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.search.backend.elasticsearch.ElasticsearchExtension;
import org.hibernate.search.backend.lucene.LuceneExtension;
import org.hibernate.search.engine.search.predicate.dsl.BooleanPredicateClausesStep;
@ -114,7 +102,6 @@ import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
@ -126,11 +113,9 @@ import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.ConceptMap;
import org.hl7.fhir.r4.model.DomainResource;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.ValueSet;
@ -139,7 +124,6 @@ import org.quartz.JobExecutionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
@ -170,7 +154,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -199,10 +182,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseTermReadSvcImpl.class);
private static final ValueSetExpansionOptions DEFAULT_EXPANSION_OPTIONS = new ValueSetExpansionOptions();
private static final TermCodeSystemVersion NO_CURRENT_VERSION = new TermCodeSystemVersion().setId(-1L);
private static boolean ourLastResultsFromTranslationCache; // For testing.
private static boolean ourLastResultsFromTranslationWithReverseCache; // For testing.
private static Runnable myInvokeOnNextCallForUnitTest;
private final int myFetchSize = DEFAULT_FETCH_SIZE;
private final Cache<String, TermCodeSystemVersion> myCodeSystemCurrentVersionCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build();
@Autowired
protected DaoRegistry myDaoRegistry;
@ -211,14 +191,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
@Autowired
protected ITermConceptDao myConceptDao;
@Autowired
protected ITermConceptMapDao myConceptMapDao;
@Autowired
protected ITermConceptMapGroupDao myConceptMapGroupDao;
@Autowired
protected ITermConceptMapGroupElementDao myConceptMapGroupElementDao;
@Autowired
protected ITermConceptMapGroupElementTargetDao myConceptMapGroupElementTargetDao;
@Autowired
protected ITermConceptPropertyDao myConceptPropertyDao;
@Autowired
protected ITermConceptDesignationDao myConceptDesignationDao;
@ -236,8 +208,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
private ITermCodeSystemVersionDao myCodeSystemVersionDao;
@Autowired
private DaoConfig myDaoConfig;
private Cache<TranslationQuery, List<TermConceptMapGroupElementTarget>> myTranslationCache;
private Cache<TranslationQuery, List<TermConceptMapGroupElement>> myTranslationWithReverseCache;
private TransactionTemplate myTxTemplate;
@Autowired
private PlatformTransactionManager myTransactionManager;
@ -257,6 +227,9 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
private ITermCodeSystemStorageSvc myConceptStorageSvc;
@Autowired
private ApplicationContext myApplicationContext;
@Autowired
private ITermConceptMappingSvc myTermConceptMappingSvc;
private volatile IValidationSupport myJpaValidationSupport;
private volatile IValidationSupport myValidationSupport;
@ -351,44 +324,9 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
*/
@VisibleForTesting
public void clearCaches() {
myTranslationCache.invalidateAll();
myTranslationWithReverseCache.invalidateAll();
myCodeSystemCurrentVersionCache.invalidateAll();
}
public void deleteConceptMap(ResourceTable theResourceTable) {
// Get existing entity so it can be deleted.
Optional<TermConceptMap> optionalExistingTermConceptMapById = myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId());
if (optionalExistingTermConceptMapById.isPresent()) {
TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get();
ourLog.info("Deleting existing TermConceptMap[{}] and its children...", existingTermConceptMap.getId());
for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) {
for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) {
for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) {
myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId());
}
myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId());
}
myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId());
}
myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId());
ourLog.info("Done deleting existing TermConceptMap[{}] and its children.", existingTermConceptMap.getId());
}
}
@Override
@Transactional
public void deleteConceptMapAndChildren(ResourceTable theResourceTable) {
deleteConceptMap(theResourceTable);
}
public void deleteValueSetForResource(ResourceTable theResourceTable) {
// Get existing entity so it can be deleted.
@ -533,8 +471,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
theAccumulator.addMessage(msg);
if (isOracleDialect()) {
expandConceptsOracle(theAccumulator, termValueSet, theFilter, theAdd);
}
else {
} else {
expandConcepts(theAccumulator, termValueSet, theFilter, theAdd);
}
}
@ -1455,7 +1392,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
private void addDisplayFilterInexact(SearchPredicateFactory f, BooleanPredicateClausesStep<?> bool, ValueSet.ConceptSetFilterComponent nextFilter) {
bool.must(f.phrase()
.field("myDisplay").boost(4.0f)
@ -1545,7 +1481,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
private void logFilteringValueOnProperty(String theValue, String theProperty) {
ourLog.debug(" * Filtering with value={} on property {}", theValue, theProperty);
}
@ -1859,26 +1794,9 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
RuleBasedTransactionAttribute rules = new RuleBasedTransactionAttribute();
rules.getRollbackRules().add(new NoRollbackRuleAttribute(ExpansionTooCostlyException.class));
myTxTemplate = new TransactionTemplate(myTransactionManager, rules);
buildTranslationCaches();
scheduleJob();
}
private void buildTranslationCaches() {
Long timeout = myDaoConfig.getTranslationCachesExpireAfterWriteInMinutes();
myTranslationCache =
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(timeout, TimeUnit.MINUTES)
.build();
myTranslationWithReverseCache =
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(timeout, TimeUnit.MINUTES)
.build();
}
public void scheduleJob() {
// Register scheduled job to pre-expand ValueSets
// In the future it would be great to make this a cluster-aware task somehow
@ -1888,163 +1806,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
mySchedulerService.scheduleClusteredJob(10 * DateUtils.MILLIS_PER_MINUTE, vsJobDefinition);
}
@Override
@Transactional
public void storeTermConceptMapAndChildren(ResourceTable theResourceTable, ConceptMap theConceptMap) {
ValidateUtil.isTrueOrThrowInvalidRequest(theResourceTable != null, "No resource supplied");
if (isPlaceholder(theConceptMap)) {
ourLog.info("Not storing TermConceptMap for placeholder {}", theConceptMap.getIdElement().toVersionless().getValueAsString());
return;
}
ValidateUtil.isNotBlankOrThrowUnprocessableEntity(theConceptMap.getUrl(), "ConceptMap has no value for ConceptMap.url");
ourLog.info("Storing TermConceptMap for {}", theConceptMap.getIdElement().toVersionless().getValueAsString());
TermConceptMap termConceptMap = new TermConceptMap();
termConceptMap.setResource(theResourceTable);
termConceptMap.setUrl(theConceptMap.getUrl());
termConceptMap.setVersion(theConceptMap.getVersion());
String source = theConceptMap.hasSourceUriType() ? theConceptMap.getSourceUriType().getValueAsString() : null;
String target = theConceptMap.hasTargetUriType() ? theConceptMap.getTargetUriType().getValueAsString() : null;
/*
* If this is a mapping between "resources" instead of purely between
* "concepts" (this is a weird concept that is technically possible, at least as of
* FHIR R4), don't try to store the mappings.
*
* See here for a description of what that is:
* http://hl7.org/fhir/conceptmap.html#bnr
*/
if ("StructureDefinition".equals(new IdType(source).getResourceType()) ||
"StructureDefinition".equals(new IdType(target).getResourceType())) {
return;
}
if (source == null && theConceptMap.hasSourceCanonicalType()) {
source = theConceptMap.getSourceCanonicalType().getValueAsString();
}
if (target == null && theConceptMap.hasTargetCanonicalType()) {
target = theConceptMap.getTargetCanonicalType().getValueAsString();
}
/*
* For now we always delete old versions. At some point, it would be nice to allow configuration to keep old versions.
*/
deleteConceptMap(theResourceTable);
/*
* Do the upload.
*/
String conceptMapUrl = termConceptMap.getUrl();
String conceptMapVersion = termConceptMap.getVersion();
Optional<TermConceptMap> optionalExistingTermConceptMapByUrl;
if (isBlank(conceptMapVersion)) {
optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndNullVersion(conceptMapUrl);
} else {
optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndVersion(conceptMapUrl, conceptMapVersion);
}
if (!optionalExistingTermConceptMapByUrl.isPresent()) {
try {
if (isNotBlank(source)) {
termConceptMap.setSource(source);
}
if (isNotBlank(target)) {
termConceptMap.setTarget(target);
}
} catch (FHIRException fe) {
throw new InternalErrorException(fe);
}
termConceptMap = myConceptMapDao.save(termConceptMap);
int codesSaved = 0;
if (theConceptMap.hasGroup()) {
TermConceptMapGroup termConceptMapGroup;
for (ConceptMap.ConceptMapGroupComponent group : theConceptMap.getGroup()) {
String groupSource = group.getSource();
if (isBlank(groupSource)) {
groupSource = source;
}
if (isBlank(groupSource)) {
throw new UnprocessableEntityException("ConceptMap[url='" + theConceptMap.getUrl() + "'] contains at least one group without a value in ConceptMap.group.source");
}
String groupTarget = group.getTarget();
if (isBlank(groupTarget)) {
groupTarget = target;
}
if (isBlank(groupTarget)) {
throw new UnprocessableEntityException("ConceptMap[url='" + theConceptMap.getUrl() + "'] contains at least one group without a value in ConceptMap.group.target");
}
termConceptMapGroup = new TermConceptMapGroup();
termConceptMapGroup.setConceptMap(termConceptMap);
termConceptMapGroup.setSource(groupSource);
termConceptMapGroup.setSourceVersion(group.getSourceVersion());
termConceptMapGroup.setTarget(groupTarget);
termConceptMapGroup.setTargetVersion(group.getTargetVersion());
myConceptMapGroupDao.save(termConceptMapGroup);
if (group.hasElement()) {
TermConceptMapGroupElement termConceptMapGroupElement;
for (ConceptMap.SourceElementComponent element : group.getElement()) {
if (isBlank(element.getCode())) {
continue;
}
termConceptMapGroupElement = new TermConceptMapGroupElement();
termConceptMapGroupElement.setConceptMapGroup(termConceptMapGroup);
termConceptMapGroupElement.setCode(element.getCode());
termConceptMapGroupElement.setDisplay(element.getDisplay());
myConceptMapGroupElementDao.save(termConceptMapGroupElement);
if (element.hasTarget()) {
TermConceptMapGroupElementTarget termConceptMapGroupElementTarget;
for (ConceptMap.TargetElementComponent elementTarget : element.getTarget()) {
if (isBlank(elementTarget.getCode())) {
continue;
}
termConceptMapGroupElementTarget = new TermConceptMapGroupElementTarget();
termConceptMapGroupElementTarget.setConceptMapGroupElement(termConceptMapGroupElement);
termConceptMapGroupElementTarget.setCode(elementTarget.getCode());
termConceptMapGroupElementTarget.setDisplay(elementTarget.getDisplay());
termConceptMapGroupElementTarget.setEquivalence(elementTarget.getEquivalence());
myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget);
if (++codesSaved % 250 == 0) {
ourLog.info("Have saved {} codes in ConceptMap", codesSaved);
myConceptMapGroupElementTargetDao.flush();
}
}
}
}
}
}
}
} else {
TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapByUrl.get();
if (isBlank(conceptMapVersion)) {
String msg = myContext.getLocalizer().getMessage(
BaseTermReadSvcImpl.class,
"cannotCreateDuplicateConceptMapUrl",
conceptMapUrl,
existingTermConceptMap.getResource().getIdDt().toUnqualifiedVersionless().getValue());
throw new UnprocessableEntityException(msg);
} else {
String msg = myContext.getLocalizer().getMessage(
BaseTermReadSvcImpl.class,
"cannotCreateDuplicateConceptMapUrlAndVersion",
conceptMapUrl, conceptMapVersion,
existingTermConceptMap.getResource().getIdDt().toUnqualifiedVersionless().getValue());
throw new UnprocessableEntityException(msg);
}
}
ourLog.info("Done storing TermConceptMap[{}] for {}", termConceptMap.getId(), theConceptMap.getIdElement().toVersionless().getValueAsString());
}
@Override
public synchronized void preExpandDeferredValueSetsToTerminologyTables() {
@ -2244,15 +2005,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
}
private boolean isPlaceholder(DomainResource theResource) {
boolean retVal = false;
Extension extension = theResource.getExtensionByUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER);
if (extension != null && extension.hasValue() && extension.getValue() instanceof BooleanType) {
retVal = ((BooleanType) extension.getValue()).booleanValue();
}
return retVal;
}
@Override
@Transactional
public IFhirResourceDaoCodeSystem.SubsumesResult subsumes(IPrimitiveType<String> theCodeA, IPrimitiveType<String> theCodeB,
@ -2362,7 +2114,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
}
private ArrayList<FhirVersionIndependentConcept> toVersionIndependentConcepts(String theSystem, Set<TermConcept> codes) {
ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>(codes.size());
for (TermConcept next : codes) {
@ -2371,254 +2122,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
return retVal;
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public List<TermConceptMapGroupElementTarget> translate(TranslationRequest theTranslationRequest) {
List<TermConceptMapGroupElementTarget> retVal = new ArrayList<>();
CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
CriteriaQuery<TermConceptMapGroupElementTarget> query = criteriaBuilder.createQuery(TermConceptMapGroupElementTarget.class);
Root<TermConceptMapGroupElementTarget> root = query.from(TermConceptMapGroupElementTarget.class);
Join<TermConceptMapGroupElementTarget, TermConceptMapGroupElement> elementJoin = root.join("myConceptMapGroupElement");
Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = elementJoin.join("myConceptMapGroup");
Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
List<TermConceptMapGroupElementTarget> cachedTargets;
ArrayList<Predicate> predicates;
Coding coding;
//-- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version
String latestConceptMapVersion = null;
if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion())
latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest);
for (TranslationQuery translationQuery : translationQueries) {
cachedTargets = myTranslationCache.getIfPresent(translationQuery);
if (cachedTargets == null) {
final List<TermConceptMapGroupElementTarget> targets = new ArrayList<>();
predicates = new ArrayList<>();
coding = translationQuery.getCoding();
if (coding.hasCode()) {
predicates.add(criteriaBuilder.equal(elementJoin.get("myCode"), coding.getCode()));
} else {
throw new InvalidRequestException("A code must be provided for translation to occur.");
}
if (coding.hasSystem()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), coding.getSystem()));
}
if (coding.hasVersion()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("mySourceVersion"), coding.getVersion()));
}
if (translationQuery.hasTargetSystem()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), translationQuery.getTargetSystem().getValueAsString()));
}
if (translationQuery.hasUrl()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl().getValueAsString()));
if (translationQuery.hasConceptMapVersion()) {
// both url and conceptMapVersion
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion().getValueAsString()));
} else {
if (StringUtils.isNotBlank(latestConceptMapVersion)) {
// only url and use latestConceptMapVersion
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion));
} else {
predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion")));
}
}
}
if (translationQuery.hasSource()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getSource().getValueAsString()));
}
if (translationQuery.hasTarget()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getTarget().getValueAsString()));
}
if (translationQuery.hasResourceId()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), translationQuery.getResourceId()));
}
Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0]));
query.where(outerPredicate);
// Use scrollable results.
final TypedQuery<TermConceptMapGroupElementTarget> typedQuery = myEntityManager.createQuery(query.select(root));
org.hibernate.query.Query<TermConceptMapGroupElementTarget> hibernateQuery = (org.hibernate.query.Query<TermConceptMapGroupElementTarget>) typedQuery;
hibernateQuery.setFetchSize(myFetchSize);
ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
try (ScrollableResultsIterator<TermConceptMapGroupElementTarget> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults)) {
while (scrollableResultsIterator.hasNext()) {
targets.add(scrollableResultsIterator.next());
}
}
ourLastResultsFromTranslationCache = false; // For testing.
myTranslationCache.get(translationQuery, k -> targets);
retVal.addAll(targets);
} else {
ourLastResultsFromTranslationCache = true; // For testing.
retVal.addAll(cachedTargets);
}
}
return retVal;
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public List<TermConceptMapGroupElement> translateWithReverse(TranslationRequest theTranslationRequest) {
List<TermConceptMapGroupElement> retVal = new ArrayList<>();
CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
CriteriaQuery<TermConceptMapGroupElement> query = criteriaBuilder.createQuery(TermConceptMapGroupElement.class);
Root<TermConceptMapGroupElement> root = query.from(TermConceptMapGroupElement.class);
Join<TermConceptMapGroupElement, TermConceptMapGroupElementTarget> targetJoin = root.join("myConceptMapGroupElementTargets");
Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = root.join("myConceptMapGroup");
Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
List<TermConceptMapGroupElement> cachedElements;
ArrayList<Predicate> predicates;
Coding coding;
//-- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version
String latestConceptMapVersion = null;
if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion())
latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest);
for (TranslationQuery translationQuery : translationQueries) {
cachedElements = myTranslationWithReverseCache.getIfPresent(translationQuery);
if (cachedElements == null) {
final List<TermConceptMapGroupElement> elements = new ArrayList<>();
predicates = new ArrayList<>();
coding = translationQuery.getCoding();
String targetCode;
String targetCodeSystem = null;
if (coding.hasCode()) {
predicates.add(criteriaBuilder.equal(targetJoin.get("myCode"), coding.getCode()));
targetCode = coding.getCode();
} else {
throw new InvalidRequestException("A code must be provided for translation to occur.");
}
if (coding.hasSystem()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), coding.getSystem()));
targetCodeSystem = coding.getSystem();
}
if (coding.hasVersion()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("myTargetVersion"), coding.getVersion()));
}
if (translationQuery.hasUrl()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl().getValueAsString()));
if (translationQuery.hasConceptMapVersion()) {
// both url and conceptMapVersion
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion().getValueAsString()));
} else {
if (StringUtils.isNotBlank(latestConceptMapVersion)) {
// only url and use latestConceptMapVersion
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion));
} else {
predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion")));
}
}
}
if (translationQuery.hasTargetSystem()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), translationQuery.getTargetSystem().getValueAsString()));
}
if (translationQuery.hasSource()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getSource().getValueAsString()));
}
if (translationQuery.hasTarget()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getTarget().getValueAsString()));
}
if (translationQuery.hasResourceId()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), translationQuery.getResourceId()));
}
Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0]));
query.where(outerPredicate);
// Use scrollable results.
final TypedQuery<TermConceptMapGroupElement> typedQuery = myEntityManager.createQuery(query.select(root));
org.hibernate.query.Query<TermConceptMapGroupElement> hibernateQuery = (org.hibernate.query.Query<TermConceptMapGroupElement>) typedQuery;
hibernateQuery.setFetchSize(myFetchSize);
ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
try (ScrollableResultsIterator<TermConceptMapGroupElement> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults)) {
while (scrollableResultsIterator.hasNext()) {
TermConceptMapGroupElement nextElement = scrollableResultsIterator.next();
// TODO: The invocation of the size() below does not seem to be necessary but for some reason, removing it causes tests in TerminologySvcImplR4Test to fail.
nextElement.getConceptMapGroupElementTargets().size();
myEntityManager.detach(nextElement);
if (isNotBlank(targetCode) && isNotBlank(targetCodeSystem)) {
for (Iterator<TermConceptMapGroupElementTarget> iter = nextElement.getConceptMapGroupElementTargets().iterator(); iter.hasNext(); ) {
TermConceptMapGroupElementTarget next = iter.next();
if (StringUtils.equals(targetCodeSystem, next.getSystem())) {
if (StringUtils.equals(targetCode, next.getCode())) {
continue;
}
}
iter.remove();
}
}
elements.add(nextElement);
}
}
ourLastResultsFromTranslationWithReverseCache = false; // For testing.
myTranslationWithReverseCache.get(translationQuery, k -> elements);
retVal.addAll(elements);
} else {
ourLastResultsFromTranslationWithReverseCache = true; // For testing.
retVal.addAll(cachedElements);
}
}
return retVal;
}
void throwInvalidValueSet(String theValueSet) {
throw new ResourceNotFoundException("Unknown ValueSet: " + UrlUtil.escapeUrlParam(theValueSet));
}
// Special case for the translate operation with url and without
// conceptMapVersion, find the latest conecptMapVersion
private String getLatestConceptMapVersion(TranslationRequest theTranslationRequest) {
Pageable page = PageRequest.of(0, 1);
List<TermConceptMap> theConceptMapList = myConceptMapDao.getTermConceptMapEntitiesByUrlOrderByMostRecentUpdate(page,
theTranslationRequest.getUrl().asStringValue());
if (!theConceptMapList.isEmpty()) {
return theConceptMapList.get(0).getVersion();
}
return null;
}
@Override
@Transactional
public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
@ -2659,7 +2162,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
return createFailureCodeValidationResult(theCodeSystem, theCode);
}
IValidationSupport.CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theValidationOptions, String theValueSetUrl, String theCodeSystem, String theCode, String theDisplay) {
IBaseResource valueSet = theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrl);
@ -2962,6 +2464,15 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
}
static boolean isPlaceholder(DomainResource theResource) {
boolean retVal = false;
Extension extension = theResource.getExtensionByUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER);
if (extension != null && extension.hasValue() && extension.getValue() instanceof BooleanType) {
retVal = ((BooleanType) extension.getValue()).booleanValue();
}
return retVal;
}
/**
* This is only used for unit tests to test failure conditions
*/
@ -3039,35 +2550,4 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
return termConcept;
}
/**
* This method is present only for unit tests, do not call from client code
*/
@VisibleForTesting
public static void clearOurLastResultsFromTranslationCache() {
ourLastResultsFromTranslationCache = false;
}
/**
* This method is present only for unit tests, do not call from client code
*/
@VisibleForTesting
public static void clearOurLastResultsFromTranslationWithReverseCache() {
ourLastResultsFromTranslationWithReverseCache = false;
}
/**
* This method is present only for unit tests, do not call from client code
*/
@VisibleForTesting
static boolean isOurLastResultsFromTranslationCache() {
return ourLastResultsFromTranslationCache;
}
/**
* This method is present only for unit tests, do not call from client code
*/
@VisibleForTesting
static boolean isOurLastResultsFromTranslationWithReverseCache() {
return ourLastResultsFromTranslationWithReverseCache;
}
}

View File

@ -0,0 +1,698 @@
package ca.uhn.fhir.jpa.term;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2021 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%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.TranslateConceptResult;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.jpa.api.model.TranslationQuery;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao;
import ca.uhn.fhir.jpa.entity.TermConceptMap;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroup;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.jpa.util.ScrollableResultsIterator;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.ValidateUtil;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.model.BooleanType;
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.ConceptMap;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import static ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.isPlaceholder;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc {
private static final Logger ourLog = LoggerFactory.getLogger(TermConceptMappingSvcImpl.class);
private static boolean ourLastResultsFromTranslationCache; // For testing.
private static boolean ourLastResultsFromTranslationWithReverseCache; // For testing.
private final int myFetchSize = BaseTermReadSvcImpl.DEFAULT_FETCH_SIZE;
@Autowired
protected ITermConceptMapDao myConceptMapDao;
@Autowired
protected ITermConceptMapGroupDao myConceptMapGroupDao;
@Autowired
protected ITermConceptMapGroupElementDao myConceptMapGroupElementDao;
@Autowired
protected ITermConceptMapGroupElementTargetDao myConceptMapGroupElementTargetDao;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
private FhirContext myContext;
@Autowired
private MemoryCacheService myMemoryCacheService;
@Override
@Transactional
public void deleteConceptMapAndChildren(ResourceTable theResourceTable) {
deleteConceptMap(theResourceTable);
}
@Override
public FhirContext getFhirContext() {
return myContext;
}
@Override
@Transactional
public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
CodeableConcept sourceCodeableConcept = new CodeableConcept();
sourceCodeableConcept
.addCoding()
.setSystem(theRequest.getSourceSystemUrl())
.setCode(theRequest.getSourceCode());
TranslationRequest request = new TranslationRequest();
request.setCodeableConcept(sourceCodeableConcept);
request.setTargetSystem(new UriType(theRequest.getTargetSystemUrl()));
return translate(request);
}
@Override
@Transactional
public void storeTermConceptMapAndChildren(ResourceTable theResourceTable, ConceptMap theConceptMap) {
ValidateUtil.isTrueOrThrowInvalidRequest(theResourceTable != null, "No resource supplied");
if (isPlaceholder(theConceptMap)) {
ourLog.info("Not storing TermConceptMap for placeholder {}", theConceptMap.getIdElement().toVersionless().getValueAsString());
return;
}
ValidateUtil.isNotBlankOrThrowUnprocessableEntity(theConceptMap.getUrl(), "ConceptMap has no value for ConceptMap.url");
ourLog.info("Storing TermConceptMap for {}", theConceptMap.getIdElement().toVersionless().getValueAsString());
TermConceptMap termConceptMap = new TermConceptMap();
termConceptMap.setResource(theResourceTable);
termConceptMap.setUrl(theConceptMap.getUrl());
termConceptMap.setVersion(theConceptMap.getVersion());
String source = theConceptMap.hasSourceUriType() ? theConceptMap.getSourceUriType().getValueAsString() : null;
String target = theConceptMap.hasTargetUriType() ? theConceptMap.getTargetUriType().getValueAsString() : null;
/*
* If this is a mapping between "resources" instead of purely between
* "concepts" (this is a weird concept that is technically possible, at least as of
* FHIR R4), don't try to store the mappings.
*
* See here for a description of what that is:
* http://hl7.org/fhir/conceptmap.html#bnr
*/
if ("StructureDefinition".equals(new IdType(source).getResourceType()) ||
"StructureDefinition".equals(new IdType(target).getResourceType())) {
return;
}
if (source == null && theConceptMap.hasSourceCanonicalType()) {
source = theConceptMap.getSourceCanonicalType().getValueAsString();
}
if (target == null && theConceptMap.hasTargetCanonicalType()) {
target = theConceptMap.getTargetCanonicalType().getValueAsString();
}
/*
* For now we always delete old versions. At some point, it would be nice to allow configuration to keep old versions.
*/
deleteConceptMap(theResourceTable);
/*
* Do the upload.
*/
String conceptMapUrl = termConceptMap.getUrl();
String conceptMapVersion = termConceptMap.getVersion();
Optional<TermConceptMap> optionalExistingTermConceptMapByUrl;
if (isBlank(conceptMapVersion)) {
optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndNullVersion(conceptMapUrl);
} else {
optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndVersion(conceptMapUrl, conceptMapVersion);
}
if (!optionalExistingTermConceptMapByUrl.isPresent()) {
try {
if (isNotBlank(source)) {
termConceptMap.setSource(source);
}
if (isNotBlank(target)) {
termConceptMap.setTarget(target);
}
} catch (FHIRException fe) {
throw new InternalErrorException(fe);
}
termConceptMap = myConceptMapDao.save(termConceptMap);
int codesSaved = 0;
if (theConceptMap.hasGroup()) {
TermConceptMapGroup termConceptMapGroup;
for (ConceptMap.ConceptMapGroupComponent group : theConceptMap.getGroup()) {
String groupSource = group.getSource();
if (isBlank(groupSource)) {
groupSource = source;
}
if (isBlank(groupSource)) {
throw new UnprocessableEntityException("ConceptMap[url='" + theConceptMap.getUrl() + "'] contains at least one group without a value in ConceptMap.group.source");
}
String groupTarget = group.getTarget();
if (isBlank(groupTarget)) {
groupTarget = target;
}
if (isBlank(groupTarget)) {
throw new UnprocessableEntityException("ConceptMap[url='" + theConceptMap.getUrl() + "'] contains at least one group without a value in ConceptMap.group.target");
}
termConceptMapGroup = new TermConceptMapGroup();
termConceptMapGroup.setConceptMap(termConceptMap);
termConceptMapGroup.setSource(groupSource);
termConceptMapGroup.setSourceVersion(group.getSourceVersion());
termConceptMapGroup.setTarget(groupTarget);
termConceptMapGroup.setTargetVersion(group.getTargetVersion());
myConceptMapGroupDao.save(termConceptMapGroup);
if (group.hasElement()) {
TermConceptMapGroupElement termConceptMapGroupElement;
for (ConceptMap.SourceElementComponent element : group.getElement()) {
if (isBlank(element.getCode())) {
continue;
}
termConceptMapGroupElement = new TermConceptMapGroupElement();
termConceptMapGroupElement.setConceptMapGroup(termConceptMapGroup);
termConceptMapGroupElement.setCode(element.getCode());
termConceptMapGroupElement.setDisplay(element.getDisplay());
myConceptMapGroupElementDao.save(termConceptMapGroupElement);
if (element.hasTarget()) {
TermConceptMapGroupElementTarget termConceptMapGroupElementTarget;
for (ConceptMap.TargetElementComponent elementTarget : element.getTarget()) {
if (isBlank(elementTarget.getCode())) {
continue;
}
termConceptMapGroupElementTarget = new TermConceptMapGroupElementTarget();
termConceptMapGroupElementTarget.setConceptMapGroupElement(termConceptMapGroupElement);
termConceptMapGroupElementTarget.setCode(elementTarget.getCode());
termConceptMapGroupElementTarget.setDisplay(elementTarget.getDisplay());
termConceptMapGroupElementTarget.setEquivalence(elementTarget.getEquivalence());
myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget);
if (++codesSaved % 250 == 0) {
ourLog.info("Have saved {} codes in ConceptMap", codesSaved);
myConceptMapGroupElementTargetDao.flush();
}
}
}
}
}
}
}
} else {
TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapByUrl.get();
if (isBlank(conceptMapVersion)) {
String msg = myContext.getLocalizer().getMessage(
BaseTermReadSvcImpl.class,
"cannotCreateDuplicateConceptMapUrl",
conceptMapUrl,
existingTermConceptMap.getResource().getIdDt().toUnqualifiedVersionless().getValue());
throw new UnprocessableEntityException(msg);
} else {
String msg = myContext.getLocalizer().getMessage(
BaseTermReadSvcImpl.class,
"cannotCreateDuplicateConceptMapUrlAndVersion",
conceptMapUrl, conceptMapVersion,
existingTermConceptMap.getResource().getIdDt().toUnqualifiedVersionless().getValue());
throw new UnprocessableEntityException(msg);
}
}
ourLog.info("Done storing TermConceptMap[{}] for {}", termConceptMap.getId(), theConceptMap.getIdElement().toVersionless().getValueAsString());
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public TranslateConceptResults translate(TranslationRequest theTranslationRequest) {
TranslateConceptResults retVal = new TranslateConceptResults();
CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
CriteriaQuery<TermConceptMapGroupElementTarget> query = criteriaBuilder.createQuery(TermConceptMapGroupElementTarget.class);
Root<TermConceptMapGroupElementTarget> root = query.from(TermConceptMapGroupElementTarget.class);
Join<TermConceptMapGroupElementTarget, TermConceptMapGroupElement> elementJoin = root.join("myConceptMapGroupElement");
Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = elementJoin.join("myConceptMapGroup");
Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
List<TranslateConceptResult> cachedTargets;
ArrayList<Predicate> predicates;
Coding coding;
//-- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version
String latestConceptMapVersion = null;
if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion())
latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest);
for (TranslationQuery translationQuery : translationQueries) {
cachedTargets = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION, translationQuery);
if (cachedTargets == null) {
final List<TranslateConceptResult> targets = new ArrayList<>();
predicates = new ArrayList<>();
coding = translationQuery.getCoding();
if (coding.hasCode()) {
predicates.add(criteriaBuilder.equal(elementJoin.get("myCode"), coding.getCode()));
} else {
throw new InvalidRequestException("A code must be provided for translation to occur.");
}
if (coding.hasSystem()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), coding.getSystem()));
}
if (coding.hasVersion()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("mySourceVersion"), coding.getVersion()));
}
if (translationQuery.hasTargetSystem()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), translationQuery.getTargetSystem().getValueAsString()));
}
if (translationQuery.hasUrl()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl().getValueAsString()));
if (translationQuery.hasConceptMapVersion()) {
// both url and conceptMapVersion
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion().getValueAsString()));
} else {
if (StringUtils.isNotBlank(latestConceptMapVersion)) {
// only url and use latestConceptMapVersion
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion));
} else {
predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion")));
}
}
}
if (translationQuery.hasSource()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getSource().getValueAsString()));
}
if (translationQuery.hasTarget()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getTarget().getValueAsString()));
}
if (translationQuery.hasResourceId()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), translationQuery.getResourceId()));
}
Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0]));
query.where(outerPredicate);
// Use scrollable results.
final TypedQuery<TermConceptMapGroupElementTarget> typedQuery = myEntityManager.createQuery(query.select(root));
org.hibernate.query.Query<TermConceptMapGroupElementTarget> hibernateQuery = (org.hibernate.query.Query<TermConceptMapGroupElementTarget>) typedQuery;
hibernateQuery.setFetchSize(myFetchSize);
ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
try (ScrollableResultsIterator<TermConceptMapGroupElementTarget> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults)) {
Set<TermConceptMapGroupElementTarget> matches = new HashSet<>();
while (scrollableResultsIterator.hasNext()) {
TermConceptMapGroupElementTarget next = scrollableResultsIterator.next();
if (matches.add(next)) {
TranslateConceptResult translationMatch = new TranslateConceptResult();
if (next.getEquivalence() != null) {
translationMatch.setEquivalence(next.getEquivalence().toCode());
}
translationMatch.setCode(next.getCode());
translationMatch.setSystem(next.getSystem());
translationMatch.setSystemVersion(next.getSystemVersion());
translationMatch.setDisplay(next.getDisplay());
translationMatch.setValueSet(next.getValueSet());
translationMatch.setSystemVersion(next.getSystemVersion());
translationMatch.setConceptMapUrl(next.getConceptMapUrl());
targets.add(translationMatch);
}
}
}
ourLastResultsFromTranslationCache = false; // For testing.
myMemoryCacheService.put(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION, translationQuery, targets);
retVal.getResults().addAll(targets);
} else {
ourLastResultsFromTranslationCache = true; // For testing.
retVal.getResults().addAll(cachedTargets);
}
}
buildTranslationResult(retVal);
return retVal;
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public TranslateConceptResults translateWithReverse(TranslationRequest theTranslationRequest) {
TranslateConceptResults retVal = new TranslateConceptResults();
CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
CriteriaQuery<TermConceptMapGroupElement> query = criteriaBuilder.createQuery(TermConceptMapGroupElement.class);
Root<TermConceptMapGroupElement> root = query.from(TermConceptMapGroupElement.class);
Join<TermConceptMapGroupElement, TermConceptMapGroupElementTarget> targetJoin = root.join("myConceptMapGroupElementTargets");
Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = root.join("myConceptMapGroup");
Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
List<TranslateConceptResult> cachedElements;
ArrayList<Predicate> predicates;
Coding coding;
//-- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version
String latestConceptMapVersion = null;
if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion())
latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest);
for (TranslationQuery translationQuery : translationQueries) {
cachedElements = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION_REVERSE, translationQuery);
if (cachedElements == null) {
final List<TranslateConceptResult> elements = new ArrayList<>();
predicates = new ArrayList<>();
coding = translationQuery.getCoding();
String targetCode;
String targetCodeSystem = null;
if (coding.hasCode()) {
predicates.add(criteriaBuilder.equal(targetJoin.get("myCode"), coding.getCode()));
targetCode = coding.getCode();
} else {
throw new InvalidRequestException("A code must be provided for translation to occur.");
}
if (coding.hasSystem()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), coding.getSystem()));
targetCodeSystem = coding.getSystem();
}
if (coding.hasVersion()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("myTargetVersion"), coding.getVersion()));
}
if (translationQuery.hasUrl()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl().getValueAsString()));
if (translationQuery.hasConceptMapVersion()) {
// both url and conceptMapVersion
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion().getValueAsString()));
} else {
if (StringUtils.isNotBlank(latestConceptMapVersion)) {
// only url and use latestConceptMapVersion
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion));
} else {
predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion")));
}
}
}
if (translationQuery.hasTargetSystem()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), translationQuery.getTargetSystem().getValueAsString()));
}
if (translationQuery.hasSource()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getSource().getValueAsString()));
}
if (translationQuery.hasTarget()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getTarget().getValueAsString()));
}
if (translationQuery.hasResourceId()) {
predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), translationQuery.getResourceId()));
}
Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0]));
query.where(outerPredicate);
// Use scrollable results.
final TypedQuery<TermConceptMapGroupElement> typedQuery = myEntityManager.createQuery(query.select(root));
org.hibernate.query.Query<TermConceptMapGroupElement> hibernateQuery = (org.hibernate.query.Query<TermConceptMapGroupElement>) typedQuery;
hibernateQuery.setFetchSize(myFetchSize);
ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
try (ScrollableResultsIterator<TermConceptMapGroupElement> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults)) {
Set<TermConceptMapGroupElementTarget> matches = new HashSet<>();
while (scrollableResultsIterator.hasNext()) {
TermConceptMapGroupElement nextElement = scrollableResultsIterator.next();
// TODO: The invocation of the size() below does not seem to be necessary but for some reason, removing it causes tests in TerminologySvcImplR4Test to fail.
nextElement.getConceptMapGroupElementTargets().size();
myEntityManager.detach(nextElement);
if (isNotBlank(targetCode)) {
for (TermConceptMapGroupElementTarget next : nextElement.getConceptMapGroupElementTargets()) {
if (matches.add(next)) {
if (isBlank(targetCodeSystem) || StringUtils.equals(targetCodeSystem, next.getSystem())) {
if (StringUtils.equals(targetCode, next.getCode())) {
TranslateConceptResult translationMatch = new TranslateConceptResult();
translationMatch.setCode(nextElement.getCode());
translationMatch.setSystem(nextElement.getSystem());
translationMatch.setSystemVersion(nextElement.getSystemVersion());
translationMatch.setDisplay(nextElement.getDisplay());
translationMatch.setValueSet(nextElement.getValueSet());
translationMatch.setSystemVersion(nextElement.getSystemVersion());
translationMatch.setConceptMapUrl(nextElement.getConceptMapUrl());
if (next.getEquivalence() != null) {
translationMatch.setEquivalence(next.getEquivalence().toCode());
}
if (alreadyContainsMapping(elements, translationMatch) || alreadyContainsMapping(retVal.getResults(), translationMatch)) {
continue;
}
elements.add(translationMatch);
}
}
}
}
}
}
}
ourLastResultsFromTranslationWithReverseCache = false; // For testing.
myMemoryCacheService.put(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION_REVERSE, translationQuery, elements);
retVal.getResults().addAll(elements);
} else {
ourLastResultsFromTranslationWithReverseCache = true; // For testing.
retVal.getResults().addAll(cachedElements);
}
}
buildTranslationResult(retVal);
return retVal;
}
private boolean alreadyContainsMapping(List<TranslateConceptResult> elements, TranslateConceptResult translationMatch) {
for (TranslateConceptResult nextExistingElement : elements) {
if (nextExistingElement.getSystem().equals(translationMatch.getSystem())) {
if (nextExistingElement.getSystemVersion().equals(translationMatch.getSystemVersion())) {
if (nextExistingElement.getCode().equals(translationMatch.getCode())) {
return true;
}
}
}
}
return false;
}
public void deleteConceptMap(ResourceTable theResourceTable) {
// Get existing entity so it can be deleted.
Optional<TermConceptMap> optionalExistingTermConceptMapById = myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId());
if (optionalExistingTermConceptMapById.isPresent()) {
TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get();
ourLog.info("Deleting existing TermConceptMap[{}] and its children...", existingTermConceptMap.getId());
for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) {
for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) {
for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) {
myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId());
}
myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId());
}
myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId());
}
myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId());
ourLog.info("Done deleting existing TermConceptMap[{}] and its children.", existingTermConceptMap.getId());
}
}
// Special case for the translate operation with url and without
// conceptMapVersion, find the latest conecptMapVersion
private String getLatestConceptMapVersion(TranslationRequest theTranslationRequest) {
Pageable page = PageRequest.of(0, 1);
List<TermConceptMap> theConceptMapList = myConceptMapDao.getTermConceptMapEntitiesByUrlOrderByMostRecentUpdate(page,
theTranslationRequest.getUrl().asStringValue());
if (!theConceptMapList.isEmpty()) {
return theConceptMapList.get(0).getVersion();
}
return null;
}
private void buildTranslationResult(TranslateConceptResults theTranslationResult) {
String msg;
if (theTranslationResult.getResults().isEmpty()) {
theTranslationResult.setResult(false);
msg = myContext.getLocalizer().getMessage(TermConceptMappingSvcImpl.class, "noMatchesFound");
theTranslationResult.setMessage(msg);
} else {
theTranslationResult.setResult(true);
msg = myContext.getLocalizer().getMessage(TermConceptMappingSvcImpl.class, "matchesFound");
theTranslationResult.setMessage(msg);
}
}
/**
* This method is present only for unit tests, do not call from client code
*/
@VisibleForTesting
public static void clearOurLastResultsFromTranslationCache() {
ourLastResultsFromTranslationCache = false;
}
/**
* This method is present only for unit tests, do not call from client code
*/
@VisibleForTesting
public static void clearOurLastResultsFromTranslationWithReverseCache() {
ourLastResultsFromTranslationWithReverseCache = false;
}
/**
* This method is present only for unit tests, do not call from client code
*/
@VisibleForTesting
static boolean isOurLastResultsFromTranslationCache() {
return ourLastResultsFromTranslationCache;
}
/**
* This method is present only for unit tests, do not call from client code
*/
@VisibleForTesting
static boolean isOurLastResultsFromTranslationWithReverseCache() {
return ourLastResultsFromTranslationWithReverseCache;
}
public static Parameters toParameters(TranslateConceptResults theTranslationResult) {
Parameters retVal = new Parameters();
retVal.addParameter().setName("result").setValue(new BooleanType(theTranslationResult.getResult()));
if (theTranslationResult.getMessage() != null) {
retVal.addParameter().setName("message").setValue(new StringType(theTranslationResult.getMessage()));
}
for (TranslateConceptResult translationMatch : theTranslationResult.getResults()) {
Parameters.ParametersParameterComponent matchParam = retVal.addParameter().setName("match");
populateTranslateMatchParts(translationMatch, matchParam);
}
return retVal;
}
private static void populateTranslateMatchParts(TranslateConceptResult theTranslationMatch, Parameters.ParametersParameterComponent theParam) {
if (theTranslationMatch.getEquivalence() != null) {
theParam.addPart().setName("equivalence").setValue(new CodeType(theTranslationMatch.getEquivalence()));
}
if (isNotBlank(theTranslationMatch.getSystem()) || isNotBlank(theTranslationMatch.getCode()) || isNotBlank(theTranslationMatch.getDisplay())) {
Coding value = new Coding(theTranslationMatch.getSystem(), theTranslationMatch.getCode(), theTranslationMatch.getDisplay());
if (isNotBlank(theTranslationMatch.getSystemVersion())) {
value.setVersion(theTranslationMatch.getSystemVersion());
}
theParam.addPart().setName("concept").setValue(value);
}
if (isNotBlank(theTranslationMatch.getConceptMapUrl())) {
theParam.addPart().setName("source").setValue(new UriType(theTranslationMatch.getConceptMapUrl()));
}
}
}

View File

@ -22,6 +22,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static org.hl7.fhir.convertors.conv30_40.ValueSet30_40.convertValueSet;
@ -61,7 +62,7 @@ public class TermReadSvcDstu3 extends BaseTermReadSvcImpl implements IValidation
@Override
public ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, IBaseResource theValueSetToExpand) {
public ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) {
try {
org.hl7.fhir.r4.model.ValueSet valueSetToExpandR4;
valueSetToExpandR4 = toCanonicalValueSet(theValueSetToExpand);

View File

@ -18,6 +18,7 @@ import org.hl7.fhir.r4.model.ValueSet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Nonnull;
import javax.transaction.Transactional;
/*
@ -59,7 +60,7 @@ public class TermReadSvcR4 extends BaseTermReadSvcImpl implements ITermReadSvcR4
@Transactional(dontRollbackOn = {ExpansionTooCostlyException.class})
@Override
public IValidationSupport.ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, IBaseResource theValueSetToExpand) {
public IValidationSupport.ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) {
ValueSet expanded = super.expandValueSet(theExpansionOptions, (ValueSet) theValueSetToExpand);
return new IValidationSupport.ValueSetExpansionOutcome(expanded);
}

View File

@ -24,6 +24,7 @@ import org.hl7.fhir.utilities.validation.ValidationOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.transaction.Transactional;
@ -56,7 +57,7 @@ public class TermReadSvcR5 extends BaseTermReadSvcImpl implements IValidationSup
@Override
@Transactional(dontRollbackOn = {ExpansionTooCostlyException.class})
public ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, IBaseResource theValueSetToExpand) {
public ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) {
ValueSet valueSetToExpand = (ValueSet) theValueSetToExpand;
org.hl7.fhir.r4.model.ValueSet expandedR4 = super.expandValueSet(theExpansionOptions, org.hl7.fhir.convertors.conv40_50.ValueSet40_50.convertValueSet(valueSetToExpand));
return new ValueSetExpansionOutcome(org.hl7.fhir.convertors.conv40_50.ValueSet40_50.convertValueSet(expandedR4));

View File

@ -0,0 +1,42 @@
package ca.uhn.fhir.jpa.term.api;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2021 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%
*/
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import org.hl7.fhir.r4.model.ConceptMap;
public interface ITermConceptMappingSvc extends IValidationSupport {
TranslateConceptResults translate(TranslationRequest theTranslationRequest);
TranslateConceptResults translateWithReverse(TranslationRequest theTranslationRequest);
void deleteConceptMapAndChildren(ResourceTable theResourceTable);
void storeTermConceptMapAndChildren(ResourceTable theResourceTable, ConceptMap theConceptMap);
}

View File

@ -91,18 +91,10 @@ public interface ITermReadSvc extends IValidationSupport {
CodeSystem fetchCanonicalCodeSystemFromCompleteContext(String theSystem);
void deleteConceptMapAndChildren(ResourceTable theResourceTable);
void deleteValueSetAndChildren(ResourceTable theResourceTable);
void storeTermConceptMapAndChildren(ResourceTable theResourceTable, ConceptMap theConceptMap);
void storeTermValueSet(ResourceTable theResourceTable, ValueSet theValueSet);
List<TermConceptMapGroupElementTarget> translate(TranslationRequest theTranslationRequest);
List<TermConceptMapGroupElement> translateWithReverse(TranslationRequest theTranslationRequest);
IFhirResourceDaoCodeSystem.SubsumesResult subsumes(IPrimitiveType<String> theCodeA, IPrimitiveType<String> theCodeB, IPrimitiveType<String> theSystem, IBaseCoding theCodingA, IBaseCoding theCodingB);
void preExpandDeferredValueSetsToTerminologyTables();

View File

@ -20,8 +20,10 @@ package ca.uhn.fhir.jpa.util;
* #L%
*/
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
import java.util.EnumMap;
@ -39,6 +41,9 @@ import java.util.function.Function;
*/
public class MemoryCacheService {
@Autowired
private DaoConfig myDaoConfig;
private EnumMap<CacheEnum, Cache<?, ?>> myCaches;
@PostConstruct
@ -47,7 +52,23 @@ public class MemoryCacheService {
myCaches = new EnumMap<>(CacheEnum.class);
for (CacheEnum next : CacheEnum.values()) {
Cache<Object, Object> nextCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).maximumSize(10000).build();
long timeoutSeconds;
switch (next) {
case CONCEPT_TRANSLATION:
case CONCEPT_TRANSLATION_REVERSE:
timeoutSeconds = myDaoConfig.getTranslationCachesExpireAfterWriteInMinutes() * 1000;
break;
case TAG_DEFINITION:
case PERSISTENT_ID:
case RESOURCE_LOOKUP:
case FORCED_ID:
default:
timeoutSeconds = 60;
break;
}
Cache<Object, Object> nextCache = Caffeine.newBuilder().expireAfterWrite(timeoutSeconds, TimeUnit.MINUTES).maximumSize(10000).build();
myCaches.put(next, nextCache);
}
@ -85,6 +106,8 @@ public class MemoryCacheService {
PERSISTENT_ID,
RESOURCE_LOOKUP,
FORCED_ID,
CONCEPT_TRANSLATION,
CONCEPT_TRANSLATION_REVERSE
}

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.packages.NpmJpaValidationSupport;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport;
@ -49,6 +50,8 @@ public class JpaValidationSupportChain extends ValidationSupportChain {
private ITermReadSvc myTerminologyService;
@Autowired
private NpmJpaValidationSupport myNpmJpaValidationSupport;
@Autowired
private ITermConceptMappingSvc myConceptMappingSvc;
public JpaValidationSupportChain(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
@ -73,6 +76,7 @@ public class JpaValidationSupportChain extends ValidationSupportChain {
addValidationSupport(new InMemoryTerminologyServerValidationSupport(myFhirContext));
addValidationSupport(myNpmJpaValidationSupport);
addValidationSupport(new CommonCodeSystemsTerminologyService(myFhirContext));
addValidationSupport(myConceptMappingSvc);
}
}

View File

@ -42,6 +42,7 @@ import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl;
import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl;
import ca.uhn.fhir.jpa.term.TermDeferredStorageSvcImpl;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
@ -363,8 +364,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
public void afterClearTerminologyCaches() {
BaseTermReadSvcImpl baseHapiTerminologySvc = AopTestUtils.getTargetObject(myTermSvc);
baseHapiTerminologySvc.clearCaches();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationCache();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache();
TermConceptMappingSvcImpl.clearOurLastResultsFromTranslationCache();
TermConceptMappingSvcImpl.clearOurLastResultsFromTranslationWithReverseCache();
TermDeferredStorageSvcImpl deferredSvc = AopTestUtils.getTargetObject(myTerminologyDeferredStorageSvc);
deferredSvc.clearDeferred();
}

View File

@ -1,17 +1,12 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Optional;
import ca.uhn.fhir.context.support.TranslateConceptResult;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.jpa.entity.TermConceptMap;
import org.hl7.fhir.dstu3.model.ConceptMap;
import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.UriType;
import org.junit.jupiter.api.BeforeEach;
@ -23,10 +18,13 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import ca.uhn.fhir.jpa.api.model.TranslationMatch;
import ca.uhn.fhir.jpa.api.model.TranslationRequest;
import ca.uhn.fhir.jpa.api.model.TranslationResult;
import ca.uhn.fhir.jpa.entity.TermConceptMap;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FhirResourceDaoDstu3ConceptMapTest extends BaseJpaDstu3Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu3ConceptMapTest.class);
@ -52,9 +50,9 @@ public class FhirResourceDaoDstu3ConceptMapTest extends BaseJpaDstu3Test {
.setCode("12345");
translationRequest.setTargetSystem(new UriType(CS_URL_3));
TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null);
TranslateConceptResults translationResult = myConceptMapDao.translate(translationRequest, null);
assertFalse(translationResult.getResult().booleanValue());
assertFalse(translationResult.getResult());
}
});
@ -76,32 +74,28 @@ public class FhirResourceDaoDstu3ConceptMapTest extends BaseJpaDstu3Test {
.setCode("12345");
translationRequest.setTargetSystem(new UriType(CS_URL_3));
TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null);
TranslateConceptResults translationResult = myConceptMapDao.translate(translationRequest, null);
assertTrue(translationResult.getResult().booleanValue());
assertEquals("Matches found!", translationResult.getMessage().getValueAsString());
assertTrue(translationResult.getResult());
assertEquals("Matches found", translationResult.getMessage());
assertEquals(2, translationResult.getMatches().size());
assertEquals(2, translationResult.getResults().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertEquals(Enumerations.ConceptMapEquivalence.EQUAL.toCode(), translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept();
assertEquals("56789", concept.getCode());
assertEquals("Target Code 56789", concept.getDisplay());
assertEquals(CS_URL_3, concept.getSystem());
assertEquals("Version 4", concept.getVersion());
assertFalse(concept.getUserSelected());
assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
TranslateConceptResult translationMatch = translationResult.getResults().get(0);
assertEquals(Enumerations.ConceptMapEquivalence.EQUAL.toCode(), translationMatch.getEquivalence());
assertEquals("56789", translationMatch.getCode());
assertEquals("Target Code 56789", translationMatch.getDisplay());
assertEquals(CS_URL_3, translationMatch.getSystem());
assertEquals("Version 4", translationMatch.getSystemVersion());
assertEquals(CM_URL, translationMatch.getConceptMapUrl());
translationMatch = translationResult.getMatches().get(1);
assertEquals(Enumerations.ConceptMapEquivalence.WIDER.toCode(), translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept();
assertEquals("67890", concept.getCode());
assertEquals("Target Code 67890", concept.getDisplay());
assertEquals(CS_URL_3, concept.getSystem());
assertEquals("Version 4", concept.getVersion());
assertFalse(concept.getUserSelected());
assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getResults().get(1);
assertEquals(Enumerations.ConceptMapEquivalence.WIDER.toCode(), translationMatch.getEquivalence());
assertEquals("67890", translationMatch.getCode());
assertEquals("Target Code 67890", translationMatch.getDisplay());
assertEquals(CS_URL_3, translationMatch.getSystem());
assertEquals("Version 4", translationMatch.getSystemVersion());
assertEquals(CM_URL, translationMatch.getConceptMapUrl());
// </editor-fold>
}
});

View File

@ -74,9 +74,11 @@ import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.search.warm.ICacheWarmingSvc;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
import ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl;
import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl;
import ca.uhn.fhir.jpa.term.TermDeferredStorageSvcImpl;
import ca.uhn.fhir.jpa.term.ValueSetExpansionR4Test;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
@ -197,6 +199,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil
private static IValidationSupport ourJpaValidationSupportChainR4;
private static IFhirResourceDaoValueSet<ValueSet, Coding, CodeableConcept> ourValueSetDao;
@Autowired
protected ITermConceptMappingSvc myConceptMappingSvc;
@Autowired
protected IPartitionLookupSvc myPartitionConfigSvc;
@Autowired
@ -510,8 +514,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil
public void afterClearTerminologyCaches() {
BaseTermReadSvcImpl baseHapiTerminologySvc = AopTestUtils.getTargetObject(myTermSvc);
baseHapiTerminologySvc.clearCaches();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationCache();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache();
TermConceptMappingSvcImpl.clearOurLastResultsFromTranslationCache();
TermConceptMappingSvcImpl.clearOurLastResultsFromTranslationWithReverseCache();
TermDeferredStorageSvcImpl termDeferredStorageSvc = AopTestUtils.getTargetObject(myTerminologyDeferredStorageSvc);
termDeferredStorageSvc.clearDeferred();
}

View File

@ -59,6 +59,7 @@ import ca.uhn.fhir.jpa.search.warm.ICacheWarmingSvc;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
import ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl;
import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl;
import ca.uhn.fhir.jpa.term.TermDeferredStorageSvcImpl;
import ca.uhn.fhir.jpa.term.ValueSetExpansionR4Test;
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
@ -437,8 +438,8 @@ public abstract class BaseJpaR5Test extends BaseJpaTest {
public void afterClearTerminologyCaches() {
BaseTermReadSvcImpl baseHapiTerminologySvc = AopTestUtils.getTargetObject(myTermSvc);
baseHapiTerminologySvc.clearCaches();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationCache();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache();
TermConceptMappingSvcImpl.clearOurLastResultsFromTranslationCache();
TermConceptMappingSvcImpl.clearOurLastResultsFromTranslationWithReverseCache();
TermDeferredStorageSvcImpl deferredStorageSvc = AopTestUtils.getTargetObject(myTermDeferredStorageSvc);
deferredStorageSvc.clearDeferred();
}

View File

@ -0,0 +1,133 @@
package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationInterceptor;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Observation;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class ResponseTerminologyTranslationInterceptorTest extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseTerminologyTranslationInterceptorTest.class);
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private IInterceptorService myInterceptorBroadcaster;
@Autowired
private ResponseTerminologyTranslationInterceptor myResponseTerminologyTranslationInterceptor;
@BeforeEach
public void beforeEach() {
myConceptMapDao.create(createConceptMap());
ourRestServer.registerInterceptor(myResponseTerminologyTranslationInterceptor);
}
@AfterEach
public void afterEach() {
myResponseTerminologyTranslationInterceptor.clearMappingSpecifications();
ourRestServer.unregisterInterceptor(myResponseTerminologyTranslationInterceptor);
}
@Test
public void testMapConcept_MappingFound() {
myResponseTerminologyTranslationInterceptor.addMappingSpecification(CS_URL, CS_URL_2);
Observation observation = new Observation();
observation.setStatus(Observation.ObservationStatus.AMENDED);
observation .getCode()
.addCoding(new Coding(CS_URL, "12345", null));
IIdType id = myObservationDao.create(observation).getId();
// Read it back
observation = myClient.read().resource(Observation.class).withId(id).execute();
assertThat(toCodeStrings(observation).toString(), toCodeStrings(observation), Matchers.contains(
"[system=http://example.com/my_code_system, code=12345, display=null]",
"[system=http://example.com/my_code_system2, code=34567, display=Target Code 34567]"
));
}
@Test
public void testMapConcept_MultipleMappingsFound() {
myResponseTerminologyTranslationInterceptor.addMappingSpecification(CS_URL, CS_URL_3);
Observation observation = new Observation();
observation.setStatus(Observation.ObservationStatus.AMENDED);
observation .getCode()
.addCoding(new Coding(CS_URL, "12345", null));
IIdType id = myObservationDao.create(observation).getId();
// Read it back
observation = myClient.read().resource(Observation.class).withId(id).execute();
assertThat(toCodeStrings(observation).toString(), toCodeStrings(observation), Matchers.contains(
"[system=http://example.com/my_code_system, code=12345, display=null]",
"[system=http://example.com/my_code_system3, code=56789, display=Target Code 56789]",
"[system=http://example.com/my_code_system3, code=67890, display=Target Code 67890]"
));
}
/**
* Don't map if we already have a code in the desired target
*/
@Test
public void testMapConcept_MappingNotNeeded() {
myResponseTerminologyTranslationInterceptor.addMappingSpecification(CS_URL, CS_URL_2);
Observation observation = new Observation();
observation.setStatus(Observation.ObservationStatus.AMENDED);
observation .getCode()
.addCoding(new Coding(CS_URL, "12345", null))
.addCoding(new Coding(CS_URL_2, "9999", "Display 9999"));
IIdType id = myObservationDao.create(observation).getId();
// Read it back
observation = myClient.read().resource(Observation.class).withId(id).execute();
assertThat(toCodeStrings(observation).toString(), toCodeStrings(observation), Matchers.contains(
"[system=http://example.com/my_code_system, code=12345, display=null]",
"[system=http://example.com/my_code_system2, code=9999, display=Display 9999]"
));
}
@Test
public void testMapConcept_NoMappingExists() {
myResponseTerminologyTranslationInterceptor.addMappingSpecification(CS_URL, CS_URL_2);
Observation observation = new Observation();
observation.setStatus(Observation.ObservationStatus.AMENDED);
observation .getCode()
.addCoding(new Coding(CS_URL, "FOO", null));
IIdType id = myObservationDao.create(observation).getId();
// Read it back
observation = myClient.read().resource(Observation.class).withId(id).execute();
assertThat(toCodeStrings(observation).toString(), toCodeStrings(observation), Matchers.contains(
"[system=http://example.com/my_code_system, code=FOO, display=null]"
));
}
@Nonnull
private List<String> toCodeStrings(Observation observation) {
return observation.getCode().getCoding().stream().map(t -> "[system=" + t.getSystem() + ", code=" + t.getCode() + ", display=" + t.getDisplay() + "]").collect(Collectors.toList());
}
}

View File

@ -95,7 +95,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -156,7 +156,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -205,7 +205,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -255,7 +255,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -304,7 +304,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -352,7 +352,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -400,7 +400,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -446,7 +446,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertFalse(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("No matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("No Matches found", ((StringType) param.getValue()).getValueAsString());
}
@ -480,7 +480,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -525,7 +525,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -570,7 +570,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -615,7 +615,7 @@ public class ResourceProviderDstu3ConceptMapTest extends BaseResourceProviderDst
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);

View File

@ -567,11 +567,6 @@ public class ConsentInterceptorResourceProviderR4Test extends BaseResourceProvid
assertEquals(1, response.getEntry().size());
assertNull(response.getTotalElement().getValue());
// Load next page
response = myClient.loadPage().next(response).execute();
assertEquals(1, response.getEntry().size());
assertNull(response.getTotalElement().getValue());
StopWatch sw = new StopWatch();
while(true) {
SearchStatusEnum status = runInTransaction(() -> {
@ -586,6 +581,11 @@ public class ConsentInterceptorResourceProviderR4Test extends BaseResourceProvid
}
}
// Load next page
response = myClient.loadPage().next(response).execute();
assertEquals(1, response.getEntry().size());
assertNull(response.getTotalElement().getValue());
runInTransaction(() -> {
Search search = mySearchEntityDao.findByUuidAndFetchIncludes(searchId).orElseThrow(() -> new IllegalStateException());
assertEquals(3, search.getNumFound());
@ -594,7 +594,12 @@ public class ConsentInterceptorResourceProviderR4Test extends BaseResourceProvid
});
// The paging should have ended now - but the last redacted female result is an empty existing page which should never have been there.
assertNull(BundleUtil.getLinkUrlOfType(myFhirCtx, response, "next"));
String next = BundleUtil.getLinkUrlOfType(myFhirCtx, response, "next");
if (next != null) {
response = myClient.loadPage().next(response).execute();
fail(myFhirCtx.newJsonParser().encodeResourceToString(response));
}
}
/**

View File

@ -94,7 +94,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -152,7 +152,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
@ -197,7 +197,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertFalse(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("No matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("No Matches found", ((StringType) param.getValue()).getValueAsString());
assertFalse(hasParameterByName(respParams, "match"));
}
@ -230,7 +230,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(3, getNumberOfParametersByName(respParams, "match"));
@ -308,7 +308,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
@ -361,7 +361,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(4, getNumberOfParametersByName(respParams, "match"));
@ -452,7 +452,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(3, getNumberOfParametersByName(respParams, "match"));
@ -531,7 +531,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
@ -582,7 +582,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));
@ -647,7 +647,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
@ -698,7 +698,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));
@ -761,7 +761,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(3, getNumberOfParametersByName(respParams, "match"));
@ -838,7 +838,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(3, getNumberOfParametersByName(respParams, "match"));
@ -949,7 +949,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -993,7 +993,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(3, getNumberOfParametersByName(respParams, "match"));
@ -1074,7 +1074,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
@ -1120,7 +1120,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertFalse(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("No matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("No Matches found", ((StringType) param.getValue()).getValueAsString());
assertFalse(hasParameterByName(respParams, "match"));
}
@ -1155,7 +1155,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));
@ -1221,7 +1221,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));
@ -1291,7 +1291,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(4, getNumberOfParametersByName(respParams, "match"));
@ -1324,9 +1324,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString());
param = getParametersByName(respParams, "match").get(2);
assertEquals(2, param.getPart().size());
assertEquals(3, param.getPart().size());
part = getPartByName(param, "equivalence");
assertFalse(part.hasValue());
assertEquals("wider", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept");
coding = (Coding) part.getValue();
assertEquals("23456", coding.getCode());
@ -1384,7 +1384,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));
@ -1451,7 +1451,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));
@ -1518,7 +1518,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
@ -1571,7 +1571,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
@ -1622,7 +1622,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));
@ -1687,7 +1687,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));
@ -1783,7 +1783,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -1828,7 +1828,7 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(2, getNumberOfParametersByName(respParams, "match"));

View File

@ -88,7 +88,7 @@ public class ResourceProviderR5ConceptMapTest extends BaseResourceProviderR5Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);
@ -170,7 +170,7 @@ public class ResourceProviderR5ConceptMapTest extends BaseResourceProviderR5Test
assertTrue(((BooleanType) param.getValue()).booleanValue());
param = getParameterByName(respParams, "message");
assertEquals("Matches found!", ((StringType) param.getValue()).getValueAsString());
assertEquals("Matches found", ((StringType) param.getValue()).getValueAsString());
assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0);

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -144,13 +144,13 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-test-utilities</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -55,13 +55,13 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-test-utilities</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -86,6 +86,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
//-- add index on HFJ_FORCED_ID
version.onTable("HFJ_FORCED_ID").addIndex("20210309.2", "IDX_FORCEID_FID")
.unique(false).withColumns("FORCED_ID");
}
private void init530() {

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -169,7 +169,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-converter</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
</dependency>
</dependencies>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -0,0 +1,99 @@
package ca.uhn.fhir.rest.server.interceptor;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2021 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%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.util.BundleUtil;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.List;
public class BaseResponseTerminologyInterceptor {
protected final IValidationSupport myValidationSupport;
protected final FhirContext myContext;
/**
* Constructor
*
* @param theValidationSupport The validation support module
*/
public BaseResponseTerminologyInterceptor(@Nonnull IValidationSupport theValidationSupport) {
myValidationSupport = theValidationSupport;
Validate.notNull(theValidationSupport, "The validation support must not be null");
myContext = theValidationSupport.getFhirContext();
Validate.notNull(myContext, "The validation support must not return a null context");
}
@Nonnull
protected List<IBaseResource> toListForProcessing(RequestDetails theRequestDetails, IBaseResource theResource) {
switch (theRequestDetails.getRestOperationType()) {
// Don't apply to these operations
case ADD_TAGS:
case DELETE_TAGS:
case GET_TAGS:
case GET_PAGE:
case GRAPHQL_REQUEST:
case EXTENDED_OPERATION_SERVER:
case EXTENDED_OPERATION_TYPE:
case EXTENDED_OPERATION_INSTANCE:
case CREATE:
case DELETE:
case TRANSACTION:
case UPDATE:
case VALIDATE:
case METADATA:
case META_ADD:
case META:
case META_DELETE:
case PATCH:
default:
return Collections.emptyList();
// Do apply to these operations
case HISTORY_INSTANCE:
case HISTORY_SYSTEM:
case HISTORY_TYPE:
case SEARCH_SYSTEM:
case SEARCH_TYPE:
case READ:
case VREAD:
break;
}
List<IBaseResource> resources;
if (theResource instanceof IBaseBundle) {
resources = BundleUtil.toListOfResources(myContext, (IBaseBundle) theResource);
} else {
resources = Collections.singletonList(theResource);
}
return resources;
}
}

View File

@ -26,6 +26,9 @@ public class InterceptorOrders {
public static final int RESPONSE_HIGHLIGHTER_INTERCEPTOR = 10000;
public static final int RESPONSE_SIZE_CAPTURING_INTERCEPTOR_COMPLETED = -1;
public static final int RESPONSE_TERMINOLOGY_TRANSLATION_INTERCEPTOR = 100;
public static final int RESPONSE_TERMINOLOGY_DISPLAY_POPULATION_INTERCEPTOR = 110;
/**
* Non instantiable
*/

View File

@ -0,0 +1,120 @@
package ca.uhn.fhir.rest.server.interceptor;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2021 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%
*/
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.IModelVisitor;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.List;
import java.util.Objects;
import static ca.uhn.fhir.rest.server.interceptor.InterceptorOrders.RESPONSE_TERMINOLOGY_DISPLAY_POPULATION_INTERCEPTOR;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This interceptor looks for coded data (
*
* @since 5.4.0
*/
public class ResponseTerminologyDisplayPopulationInterceptor extends BaseResponseTerminologyInterceptor {
private final BaseRuntimeChildDefinition myCodingSystemChild;
private final BaseRuntimeChildDefinition myCodingCodeChild;
private final Class<? extends IBase> myCodingType;
private final BaseRuntimeElementCompositeDefinition<?> myCodingDefinitition;
private final BaseRuntimeChildDefinition myCodingDisplayChild;
private final RuntimePrimitiveDatatypeDefinition myStringDefinition;
/**
* Constructor
*
* @param theValidationSupport The validation support module
*/
public ResponseTerminologyDisplayPopulationInterceptor(IValidationSupport theValidationSupport) {
super(theValidationSupport);
myCodingDefinitition = (BaseRuntimeElementCompositeDefinition<?>) Objects.requireNonNull(myContext.getElementDefinition("Coding"));
myCodingType = myCodingDefinitition.getImplementingClass();
myCodingSystemChild = myCodingDefinitition.getChildByName("system");
myCodingCodeChild = myCodingDefinitition.getChildByName("code");
myCodingDisplayChild = myCodingDefinitition.getChildByName("display");
myStringDefinition = (RuntimePrimitiveDatatypeDefinition) myContext.getElementDefinition("string");
}
@Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE, order = RESPONSE_TERMINOLOGY_DISPLAY_POPULATION_INTERCEPTOR)
public void handleResource(RequestDetails theRequestDetails, IBaseResource theResource) {
List<IBaseResource> resources = toListForProcessing(theRequestDetails, theResource);
FhirTerser terser = myContext.newTerser();
for (IBaseResource nextResource : resources) {
terser.visit(nextResource, new MappingVisitor());
}
}
private class MappingVisitor implements IModelVisitor {
@Override
public void acceptElement(IBaseResource theResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition) {
if (myCodingType.isAssignableFrom(theElement.getClass())) {
String system = myCodingSystemChild.getAccessor().getFirstValueOrNull(theElement).map(t -> (IPrimitiveType<?>) t).map(t -> t.getValueAsString()).orElse(null);
String code = myCodingCodeChild.getAccessor().getFirstValueOrNull(theElement).map(t -> (IPrimitiveType<?>) t).map(t -> t.getValueAsString()).orElse(null);
if (isBlank(system) || isBlank(code)) {
return;
}
String display = myCodingDisplayChild.getAccessor().getFirstValueOrNull(theElement).map(t -> (IPrimitiveType<?>) t).map(t -> t.getValueAsString()).orElse(null);
if (isNotBlank(display)) {
return;
}
ValidationSupportContext validationSupportContext = new ValidationSupportContext(myValidationSupport);
if (myValidationSupport.isCodeSystemSupported(validationSupportContext, system)) {
IValidationSupport.LookupCodeResult lookupCodeResult = myValidationSupport.lookupCode(validationSupportContext, system, code);
if (lookupCodeResult.isFound()) {
String newDisplay = lookupCodeResult.getCodeDisplay();
IPrimitiveType<?> newString = myStringDefinition.newInstance(newDisplay);
myCodingDisplayChild.getMutator().addValue(theElement, newString);
}
}
}
}
}
}

View File

@ -0,0 +1,191 @@
package ca.uhn.fhir.rest.server.interceptor;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2021 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%
*/
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.TranslateConceptResult;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.IModelVisitor;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static ca.uhn.fhir.rest.server.interceptor.InterceptorOrders.RESPONSE_TERMINOLOGY_TRANSLATION_INTERCEPTOR;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This interceptor leverages ConceptMap resources stored in the repository to automatically map
* terminology from one CodeSystem to another at runtime, in resources that are being
* returned by the server.
* <p>
* Mappings are applied only if they are explicitly configured in the interceptor via
* the {@link #addMappingSpecification(String, String)} method.
* </p>
*
* @since 5.4.0
*/
public class ResponseTerminologyTranslationInterceptor extends BaseResponseTerminologyInterceptor {
private final BaseRuntimeChildDefinition myCodingSystemChild;
private final BaseRuntimeChildDefinition myCodingCodeChild;
private final BaseRuntimeElementDefinition<IPrimitiveType<?>> myUriDefinition;
private final BaseRuntimeElementDefinition<IPrimitiveType<?>> myCodeDefinition;
private final Class<? extends IBase> myCodeableConceptType;
private final Class<? extends IBase> myCodingType;
private final BaseRuntimeChildDefinition myCodeableConceptCodingChild;
private final BaseRuntimeElementCompositeDefinition<?> myCodingDefinitition;
private final RuntimePrimitiveDatatypeDefinition myStringDefinition;
private final BaseRuntimeChildDefinition myCodingDisplayChild;
private Map<String, String> myMappingSpecifications = new HashMap<>();
/**
* Constructor
*
* @param theValidationSupport The validation support module
*/
public ResponseTerminologyTranslationInterceptor(IValidationSupport theValidationSupport) {
super(theValidationSupport);
BaseRuntimeElementCompositeDefinition<?> codeableConceptDef = (BaseRuntimeElementCompositeDefinition<?>) Objects.requireNonNull(myContext.getElementDefinition("CodeableConcept"));
myCodeableConceptType = codeableConceptDef.getImplementingClass();
myCodeableConceptCodingChild = codeableConceptDef.getChildByName("coding");
myCodingDefinitition = (BaseRuntimeElementCompositeDefinition<?>) Objects.requireNonNull(myContext.getElementDefinition("Coding"));
myCodingType = myCodingDefinitition.getImplementingClass();
myCodingSystemChild = myCodingDefinitition.getChildByName("system");
myCodingCodeChild = myCodingDefinitition.getChildByName("code");
myCodingDisplayChild = myCodingDefinitition.getChildByName("display");
myUriDefinition = (RuntimePrimitiveDatatypeDefinition) myContext.getElementDefinition("uri");
myCodeDefinition = (RuntimePrimitiveDatatypeDefinition) myContext.getElementDefinition("code");
myStringDefinition = (RuntimePrimitiveDatatypeDefinition) myContext.getElementDefinition("string");
}
/**
* Adds a mapping specification using only a source and target CodeSystem URL. Any mappings specified using
* this URL
*
* @param theSourceCodeSystemUrl The source CodeSystem URL
* @param theTargetCodeSystemUrl The target CodeSystem URL
*/
public void addMappingSpecification(String theSourceCodeSystemUrl, String theTargetCodeSystemUrl) {
Validate.notBlank(theSourceCodeSystemUrl, "theSourceCodeSystemUrl must not be null or blank");
Validate.notBlank(theTargetCodeSystemUrl, "theTargetCodeSystemUrl must not be null or blank");
myMappingSpecifications.put(theSourceCodeSystemUrl, theTargetCodeSystemUrl);
}
/**
* Clear all mapping specifications
*/
public void clearMappingSpecifications() {
myMappingSpecifications.clear();
}
@Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE, order = RESPONSE_TERMINOLOGY_TRANSLATION_INTERCEPTOR)
public void handleResource(RequestDetails theRequestDetails, IBaseResource theResource) {
List<IBaseResource> resources = toListForProcessing(theRequestDetails, theResource);
FhirTerser terser = myContext.newTerser();
for (IBaseResource nextResource : resources) {
terser.visit(nextResource, new MappingVisitor());
}
}
private class MappingVisitor implements IModelVisitor {
@Override
public void acceptElement(IBaseResource theResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition) {
if (myCodeableConceptType.isAssignableFrom(theElement.getClass())) {
// Find all existing Codings
Multimap<String, String> foundSystemsToCodes = ArrayListMultimap.create();
List<IBase> nextCodeableConceptCodings = myCodeableConceptCodingChild.getAccessor().getValues(theElement);
for (IBase nextCodeableConceptCoding : nextCodeableConceptCodings) {
String system = myCodingSystemChild.getAccessor().getFirstValueOrNull(nextCodeableConceptCoding).map(t -> (IPrimitiveType<?>) t).map(t -> t.getValueAsString()).orElse(null);
String code = myCodingCodeChild.getAccessor().getFirstValueOrNull(nextCodeableConceptCoding).map(t -> (IPrimitiveType<?>) t).map(t -> t.getValueAsString()).orElse(null);
if (isNotBlank(system) && isNotBlank(code) && !foundSystemsToCodes.containsKey(system)) {
foundSystemsToCodes.put(system, code);
}
}
// Look for mappings
for (String nextSourceSystem : foundSystemsToCodes.keySet()) {
String wantTargetSystem = myMappingSpecifications.get(nextSourceSystem);
if (wantTargetSystem != null) {
if (!foundSystemsToCodes.containsKey(wantTargetSystem)) {
for (String code : foundSystemsToCodes.get(nextSourceSystem)) {
TranslateConceptResults translateConceptResults = myValidationSupport.translateConcept(new IValidationSupport.TranslateCodeRequest(nextSourceSystem, code, wantTargetSystem));
if (translateConceptResults != null) {
List<TranslateConceptResult> mappings = translateConceptResults.getResults();
for (TranslateConceptResult nextMapping : mappings) {
IBase newCoding = createCodingFromMappingTarget(nextMapping);
// Add coding to existing CodeableConcept
myCodeableConceptCodingChild.getMutator().addValue(theElement, newCoding);
}
}
}
}
}
}
}
}
private IBase createCodingFromMappingTarget(TranslateConceptResult nextMapping) {
IBase newCoding = myCodingDefinitition.newInstance();
IPrimitiveType<?> newSystem = myUriDefinition.newInstance(nextMapping.getSystem());
myCodingSystemChild.getMutator().addValue(newCoding, newSystem);
IPrimitiveType<?> newCode = myCodeDefinition.newInstance(nextMapping.getCode());
myCodingCodeChild.getMutator().addValue(newCoding, newCode);
if (isNotBlank(nextMapping.getDisplay())) {
IPrimitiveType<?> newDisplay = myStringDefinition.newInstance(nextMapping.getDisplay());
myCodingDisplayChild.getMutator().addValue(newCoding, newDisplay);
}
return newCoding;
}
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.4.0-PRE1-SNAPSHOT</version>
<version>5.4.0-PRE2-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,6 +3,7 @@ package org.hl7.fhir.common.hapi.validation.support;
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.TranslateConceptResults;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import org.apache.commons.lang3.Validate;
@ -69,7 +70,7 @@ public class BaseValidationSupportWrapper extends BaseValidationSupport {
}
@Override
public IValidationSupport.ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, IBaseResource theValueSetToExpand) {
public IValidationSupport.ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) {
return myWrap.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand);
}
@ -99,5 +100,8 @@ public class BaseValidationSupportWrapper extends BaseValidationSupport {
return myWrap.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet);
}
@Override
public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
return myWrap.translateConcept(theRequest);
}
}

View File

@ -3,9 +3,11 @@ package org.hl7.fhir.common.hapi.validation.support;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger;
@ -28,24 +30,44 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple
private static final Logger ourLog = LoggerFactory.getLogger(CachingValidationSupport.class);
private final Cache<String, Object> myCache;
private final Cache<String, Object> myValidateCodeCache;
private final Cache<TranslateCodeRequest, Object> myTranslateCodeCache;
private final Cache<String, Object> myLookupCodeCache;
/**
* Constuctor with default timeouts
*
* @param theWrap The validation support module to wrap
*/
public CachingValidationSupport(IValidationSupport theWrap) {
this(theWrap, CacheTimeouts.defaultValues());
}
/**
* Constructor with configurable timeouts
*
* @param theWrap The validation support module to wrap
* @param theCacheTimeouts The timeouts to use
*/
public CachingValidationSupport(IValidationSupport theWrap, CacheTimeouts theCacheTimeouts) {
super(theWrap.getFhirContext(), theWrap);
myValidateCodeCache = Caffeine
.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.expireAfterWrite(theCacheTimeouts.getValidateCodeMillis(), TimeUnit.MILLISECONDS)
.maximumSize(5000)
.build();
myLookupCodeCache = Caffeine
.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.expireAfterWrite(theCacheTimeouts.getLookupCodeMillis(), TimeUnit.MILLISECONDS)
.maximumSize(5000)
.build();
myTranslateCodeCache = Caffeine
.newBuilder()
.expireAfterWrite(theCacheTimeouts.getTranslateCodeMillis(), TimeUnit.MILLISECONDS)
.maximumSize(5000)
.build();
myCache = Caffeine
.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.expireAfterWrite(theCacheTimeouts.getMiscMillis(), TimeUnit.MILLISECONDS)
.maximumSize(5000)
.build();
}
@ -88,19 +110,6 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple
return loadFromCache(myLookupCodeCache, key, t -> super.lookupCode(theValidationSupportContext, theSystem, theCode));
}
@SuppressWarnings("OptionalAssignedToNull")
@Nullable
private <T> T loadFromCache(Cache theCache, String theKey, Function<String, T> theLoader) {
ourLog.trace("Fetching from cache: {}", theKey);
Function<String, Optional<T>> loaderWrapper = key -> Optional.ofNullable(theLoader.apply(theKey));
Optional<T> result = (Optional<T>) theCache.get(theKey, loaderWrapper);
assert result != null;
return result.orElse(null);
}
@Override
public IValidationSupport.CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theValidationOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
@ -114,10 +123,83 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple
return super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet);
}
@Override
public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
return loadFromCache(myTranslateCodeCache, theRequest, k -> super.translateConcept(theRequest));
}
@SuppressWarnings("OptionalAssignedToNull")
@Nullable
private <S, T> T loadFromCache(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
ourLog.trace("Fetching from cache: {}", theKey);
Function<S, Optional<T>> loaderWrapper = key -> Optional.ofNullable(theLoader.apply(theKey));
Optional<T> result = (Optional<T>) theCache.get(theKey, loaderWrapper);
assert result != null;
return result.orElse(null);
}
@Override
public void invalidateCaches() {
myLookupCodeCache.invalidateAll();
myCache.invalidateAll();
myValidateCodeCache.invalidateAll();
}
/**
* @since 5.4.0
*/
public static class CacheTimeouts {
private long myTranslateCodeMillis;
private long myLookupCodeMillis;
private long myValidateCodeMillis;
private long myMiscMillis;
public long getTranslateCodeMillis() {
return myTranslateCodeMillis;
}
public CacheTimeouts setTranslateCodeMillis(long theTranslateCodeMillis) {
myTranslateCodeMillis = theTranslateCodeMillis;
return this;
}
public long getLookupCodeMillis() {
return myLookupCodeMillis;
}
public CacheTimeouts setLookupCodeMillis(long theLookupCodeMillis) {
myLookupCodeMillis = theLookupCodeMillis;
return this;
}
public long getValidateCodeMillis() {
return myValidateCodeMillis;
}
public CacheTimeouts setValidateCodeMillis(long theValidateCodeMillis) {
myValidateCodeMillis = theValidateCodeMillis;
return this;
}
public long getMiscMillis() {
return myMiscMillis;
}
public CacheTimeouts setMiscMillis(long theMiscMillis) {
myMiscMillis = theMiscMillis;
return this;
}
public static CacheTimeouts defaultValues() {
return new CacheTimeouts()
.setLookupCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
.setTranslateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
.setValidateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
.setMiscMillis(10 * DateUtils.MILLIS_PER_MINUTE);
}
}
}

View File

@ -57,7 +57,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu
}
@Override
public ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, IBaseResource theValueSetToExpand) {
public ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) {
org.hl7.fhir.r5.model.ValueSet expansionR5 = expandValueSetToCanonical(theValidationSupportContext, theValueSetToExpand, null, null);
if (expansionR5 == null) {

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