From cd4973f1b79895aee14f50598d6dfb9617757a17 Mon Sep 17 00:00:00 2001 From: italomacedo Date: Thu, 4 Dec 2025 09:39:25 -0300 Subject: [PATCH] $translate using code, coding, codeableConcept, but system is currently required --- .../fhir/caching/TranslateKeyGenerator.java | 60 ++++ .../converter/MappingParametersConverter.java | 54 +++ .../fhir/hapi/HapiRestfulServer.java | 6 +- .../com/gointerop/fhir/model/Mapping.java | 314 ++++++++++++++++++ .../fhir/provider/ConceptMapProvider.java | 56 ++++ .../fhir/repository/MappingRepository.java | 80 +++++ .../fhir/service/MappingService.java | 192 +++++++++++ 7 files changed, 761 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gointerop/fhir/caching/TranslateKeyGenerator.java create mode 100644 src/main/java/com/gointerop/fhir/converter/MappingParametersConverter.java create mode 100644 src/main/java/com/gointerop/fhir/model/Mapping.java create mode 100644 src/main/java/com/gointerop/fhir/provider/ConceptMapProvider.java create mode 100644 src/main/java/com/gointerop/fhir/repository/MappingRepository.java create mode 100644 src/main/java/com/gointerop/fhir/service/MappingService.java diff --git a/src/main/java/com/gointerop/fhir/caching/TranslateKeyGenerator.java b/src/main/java/com/gointerop/fhir/caching/TranslateKeyGenerator.java new file mode 100644 index 0000000..b470557 --- /dev/null +++ b/src/main/java/com/gointerop/fhir/caching/TranslateKeyGenerator.java @@ -0,0 +1,60 @@ +package com.gointerop.fhir.caching; + +import java.lang.reflect.Method; + +import org.apache.commons.codec.digest.DigestUtils; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.ConceptMap; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.stereotype.Component; + +import com.gointerop.fhir.utils.FHIRUtil; + +@Component +public class TranslateKeyGenerator implements KeyGenerator { + + @Autowired + private FHIRUtil fhirUtil; + + @Override + public Object generate(Object target, Method method, Object... params) { + String url = (String) params[0]; + ConceptMap conceptMap = (ConceptMap) params[1]; + String conceptMapVersion = (String) params[2]; + String code = (String) params[3]; + String system = (String) params[4]; + String version = (String) params[5]; + Coding coding = (Coding) params[6]; + CodeableConcept codeableConcept = (CodeableConcept) params[7]; + String targetParam = (String) params[8]; + String targetSystem = (String) params[9]; + Boolean reverse = (Boolean) params[10]; + String xSystemCacheId = (String) params[11]; + + String txKey; + if (conceptMap != null && conceptMap.getUrl() != null) { + txKey = conceptMap.getUrl(); + } else if (conceptMap != null) { + String json = fhirUtil.getIParser().encodeResourceToString(conceptMap); + txKey = DigestUtils.md5Hex(json); + } else { + txKey = "null-resource"; + } + + return String.join("_", + url, + txKey, + conceptMapVersion, + code, + system, + version, + String.valueOf(coding != null ? coding.hashCode() : "null"), + String.valueOf(codeableConcept != null ? codeableConcept.hashCode() : "null"), + targetParam, + targetSystem, + String.valueOf(reverse), + xSystemCacheId); + } +} \ No newline at end of file diff --git a/src/main/java/com/gointerop/fhir/converter/MappingParametersConverter.java b/src/main/java/com/gointerop/fhir/converter/MappingParametersConverter.java new file mode 100644 index 0000000..38e739a --- /dev/null +++ b/src/main/java/com/gointerop/fhir/converter/MappingParametersConverter.java @@ -0,0 +1,54 @@ +package com.gointerop.fhir.converter; + +import java.util.Arrays; +import java.util.List; + +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.Parameters; +import org.hl7.fhir.r4.model.StringType; +import org.springframework.stereotype.Component; + +import com.gointerop.fhir.model.Mapping; +import com.gointerop.fhir.model.Source; + +@Component +public class MappingParametersConverter { + public Parameters toParameters(List mapping, List targetSources) { + Parameters params = new Parameters(); + + params.addParameter().setName("result").setValue(new BooleanType(true)); + + for (int i = 0; i < mapping.size(); i++) { + Mapping map = mapping.get(i); + Source targetSource = targetSources.get(i); + params.addParameter().setName("match").setPart(Arrays.asList( + new Parameters.ParametersParameterComponent().setName("equivalence") + .setValue(new CodeType(toFHIREquivalence(map.getMapType()))), + new Parameters.ParametersParameterComponent().setName("concept") + .setValue(new Coding().setCode(map.getToConceptCode()).setDisplay( + map.getToConceptNameResolved())), + new Parameters.ParametersParameterComponent().setName("source") + .setValue(new StringType(targetSource.getCanonicalUrl())) + )); + } + + return params; + } + + public String toFHIREquivalence(String oclEquivalence) { + switch (oclEquivalence) { + case "SAME-AS": + return "equivalent"; + case "NARROWER-THAN": + return "narrower"; + case "BROADER-THAN": + return "broader"; + case "NOT-EQUIVALENT": + return "disjoint"; + default: + return "unmatched"; + } + } +} diff --git a/src/main/java/com/gointerop/fhir/hapi/HapiRestfulServer.java b/src/main/java/com/gointerop/fhir/hapi/HapiRestfulServer.java index aef4528..f9d3ef3 100644 --- a/src/main/java/com/gointerop/fhir/hapi/HapiRestfulServer.java +++ b/src/main/java/com/gointerop/fhir/hapi/HapiRestfulServer.java @@ -11,8 +11,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import com.gointerop.fhir.interceptor.CapabilityStatementCustomizer; import com.gointerop.fhir.provider.CodeSystemProvider; +import com.gointerop.fhir.provider.ConceptMapProvider; import com.gointerop.fhir.provider.TerminologyCapabilitiesProvider; import com.gointerop.fhir.provider.ValueSetProvider; import com.gointerop.fhir.service.TerminologyCapabilitiesService; @@ -46,6 +46,9 @@ public class HapiRestfulServer extends RestfulServer { @Autowired private ValueSetProvider valueSetProvider; + @Autowired + private ConceptMapProvider conceptMapProvider; + @Override protected void initialize() { // Leniencia de handler para atributos (Para o TerminologyCapabilities @@ -75,6 +78,7 @@ public void unknownReference(IParseLocation theLocation, String theReference) { registerProvider(codeSystemProvider); registerProvider(valueSetProvider); + registerProvider(conceptMapProvider); // ui registerInterceptor(new ResponseHighlighterInterceptor() { diff --git a/src/main/java/com/gointerop/fhir/model/Mapping.java b/src/main/java/com/gointerop/fhir/model/Mapping.java new file mode 100644 index 0000000..17edbc9 --- /dev/null +++ b/src/main/java/com/gointerop/fhir/model/Mapping.java @@ -0,0 +1,314 @@ +package com.gointerop.fhir.model; + +import java.io.Serializable; +import java.util.function.BiFunction; + +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.ConceptMap.ConceptMapGroupComponent; +import org.hl7.fhir.r4.model.ConceptMap.SourceElementComponent; +import org.hl7.fhir.r4.model.ConceptMap.TargetElementComponent; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Mapping implements Serializable { + + private static final long serialVersionUID = 1L; + + private String id; + private String url; + + @JsonProperty("map_type") + private String mapType; + + @JsonProperty("retired") + private Boolean retired; + + @JsonProperty("from_concept_code") + private String fromConceptCode; + + @JsonProperty("from_concept_name") + private String fromConceptName; + + @JsonProperty("from_concept_name_resolved") + private String fromConceptNameResolved; + + @JsonProperty("from_concept_url") + private String fromConceptUrl; + + @JsonProperty("from_source_owner") + private String fromSourceOwner; + + @JsonProperty("from_source_owner_type") + private String fromSourceOwnerType; + + @JsonProperty("from_source") + private String fromSource; + + @JsonProperty("from_source_name") + private String fromSourceName; + + @JsonProperty("from_source_url") + private String fromSourceUrl; + + @JsonProperty("from_source_version") + private String fromSourceVersion; + + @JsonProperty("from_collection_owner") + private String fromCollectionOwner; + + @JsonProperty("from_collection") + private String fromCollection; + + @JsonProperty("from_collection_url") + private String fromCollectionUrl; + + @JsonProperty("to_concept_code") + private String toConceptCode; + + @JsonProperty("to_concept_name") + private String toConceptName; + + @JsonProperty("to_concept_name_resolved") + private String toConceptNameResolved; + + @JsonProperty("to_concept_url") + private String toConceptUrl; + + @JsonProperty("to_source_owner") + private String toSourceOwner; + + @JsonProperty("to_source_owner_type") + private String toSourceOwnerType; + + @JsonProperty("to_source") + private String toSource; + + @JsonProperty("to_source_name") + private String toSourceName; + + @JsonProperty("to_source_url") + private String toSourceUrl; + + @JsonProperty("to_source_version") + private String toSourceVersion; + + @JsonProperty("to_collection_owner") + private String toCollectionOwner; + + @JsonProperty("to_collection") + private String toCollection; + + @JsonProperty("to_collection_url") + private String toCollectionUrl; + + public Coding toTargetCoding( + BiFunction sourceResolver, + BiFunction collectionResolver) { + if (!StringUtils.hasText(toConceptCode)) { + return null; + } + + Coding coding = new Coding(); + coding.setCode(toConceptCode); + coding.setDisplay(firstNonBlank(toConceptNameResolved, toConceptName)); + + String system = getToSourceCanonical(sourceResolver); + if (system == null) { + system = getToCollectionCanonical(collectionResolver); + } + if (system == null) { + system = toConceptUrl; + } + coding.setSystem(system); + return coding; + } + + public Coding toSourceCoding( + BiFunction sourceResolver, + BiFunction collectionResolver) { + if (!StringUtils.hasText(fromConceptCode)) { + return null; + } + + Coding coding = new Coding(); + coding.setCode(fromConceptCode); + coding.setDisplay(firstNonBlank(fromConceptNameResolved, fromConceptName)); + + String system = getFromSourceCanonical(sourceResolver); + if (system == null) { + system = getFromCollectionCanonical(collectionResolver); + } + if (system == null) { + system = fromConceptUrl; + } + coding.setSystem(system); + return coding; + } + + public String getToSourceCanonical(BiFunction sourceResolver) { + String owner = firstNonBlank(toSourceOwner, ownerFromUrl(toSourceUrl)); + String identifier = firstNonBlank(toSource, sourceSlugFromUrl(toSourceUrl)); + + if (StringUtils.hasText(owner) && StringUtils.hasText(identifier)) { + Source source = sourceResolver.apply(owner, identifier); + if (source != null && StringUtils.hasText(source.getCanonicalUrl())) { + return source.getCanonicalUrl(); + } + } + + if (StringUtils.hasText(toSourceUrl)) { + return toSourceUrl; + } + return null; + } + + public String getToCollectionCanonical(BiFunction collectionResolver) { + String owner = firstNonBlank(toCollectionOwner, ownerFromCollectionUrl(toCollectionUrl)); + String identifier = firstNonBlank(toCollection, collectionSlugFromUrl(toCollectionUrl)); + + if (StringUtils.hasText(owner) && StringUtils.hasText(identifier)) { + Collection collection = collectionResolver.apply(owner, identifier); + if (collection != null && StringUtils.hasText(collection.getCanonicalUrl())) { + return collection.getCanonicalUrl(); + } + } + + if (StringUtils.hasText(toCollectionUrl)) { + return toCollectionUrl; + } + return null; + } + + public String getFromSourceCanonical(BiFunction sourceResolver) { + String owner = firstNonBlank(fromSourceOwner, ownerFromUrl(fromSourceUrl)); + String identifier = firstNonBlank(fromSource, sourceSlugFromUrl(fromSourceUrl)); + + if (StringUtils.hasText(owner) && StringUtils.hasText(identifier)) { + Source source = sourceResolver.apply(owner, identifier); + if (source != null && StringUtils.hasText(source.getCanonicalUrl())) { + return source.getCanonicalUrl(); + } + } + + if (StringUtils.hasText(fromSourceUrl)) { + return fromSourceUrl; + } + return null; + } + + public String getFromCollectionCanonical(BiFunction collectionResolver) { + String owner = firstNonBlank(fromCollectionOwner, ownerFromCollectionUrl(fromCollectionUrl)); + String identifier = firstNonBlank(fromCollection, collectionSlugFromUrl(fromCollectionUrl)); + + if (StringUtils.hasText(owner) && StringUtils.hasText(identifier)) { + Collection collection = collectionResolver.apply(owner, identifier); + if (collection != null && StringUtils.hasText(collection.getCanonicalUrl())) { + return collection.getCanonicalUrl(); + } + } + + if (StringUtils.hasText(fromCollectionUrl)) { + return fromCollectionUrl; + } + return null; + } + + public static Mapping fromConceptMap( + ConceptMapGroupComponent group, + SourceElementComponent element, + TargetElementComponent target, + boolean reverse) { + Mapping mapping = new Mapping(); + if (target != null && target.getEquivalence() != null) { + mapping.setMapType(target.getEquivalence().toCode()); + } + + if (reverse) { + mapping.setToConceptCode(element.getCode()); + mapping.setToConceptName(element.getDisplay()); + mapping.setToConceptUrl(group.getSource()); + + mapping.setFromConceptCode(target.getCode()); + mapping.setFromConceptName(target.getDisplay()); + mapping.setFromConceptUrl(group.getTarget()); + } else { + mapping.setFromConceptCode(element.getCode()); + mapping.setFromConceptName(element.getDisplay()); + mapping.setFromConceptUrl(group.getSource()); + + mapping.setToConceptCode(target.getCode()); + mapping.setToConceptName(target.getDisplay()); + mapping.setToConceptUrl(group.getTarget()); + } + + return mapping; + } + + private String firstNonBlank(String primary, String secondary) { + if (StringUtils.hasText(primary)) { + return primary; + } + if (StringUtils.hasText(secondary)) { + return secondary; + } + return null; + } + + private String ownerFromUrl(String url) { + if (!StringUtils.hasText(url)) { + return null; + } + String[] segments = url.split("/"); + for (int i = 0; i < segments.length - 1; i++) { + if ("orgs".equals(segments[i]) && StringUtils.hasText(segments[i + 1])) { + return segments[i + 1]; + } + } + return null; + } + + private String sourceSlugFromUrl(String url) { + if (!StringUtils.hasText(url)) { + return null; + } + String[] segments = url.split("/"); + for (int i = 0; i < segments.length - 1; i++) { + if ("sources".equals(segments[i]) && StringUtils.hasText(segments[i + 1])) { + return segments[i + 1]; + } + } + return null; + } + + private String ownerFromCollectionUrl(String url) { + if (!StringUtils.hasText(url)) { + return null; + } + String[] segments = url.split("/"); + for (int i = 0; i < segments.length - 1; i++) { + if ("collections".equals(segments[i]) && i >= 2 && "orgs".equals(segments[i - 2])) { + return segments[i - 1]; + } + } + return ownerFromUrl(url); + } + + private String collectionSlugFromUrl(String url) { + if (!StringUtils.hasText(url)) { + return null; + } + String[] segments = url.split("/"); + for (int i = 0; i < segments.length - 1; i++) { + if ("collections".equals(segments[i]) && StringUtils.hasText(segments[i + 1])) { + return segments[i + 1]; + } + } + return null; + } +} diff --git a/src/main/java/com/gointerop/fhir/provider/ConceptMapProvider.java b/src/main/java/com/gointerop/fhir/provider/ConceptMapProvider.java new file mode 100644 index 0000000..9dbc432 --- /dev/null +++ b/src/main/java/com/gointerop/fhir/provider/ConceptMapProvider.java @@ -0,0 +1,56 @@ +package com.gointerop.fhir.provider; + +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.CodeableConcept; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; + +import com.gointerop.fhir.service.MappingService; + +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; + +@Controller +public class ConceptMapProvider { + + @Autowired + private MappingService service; + + @Operation(name = "$translate", idempotent = true, type = ConceptMap.class) + public IBaseResource translate( + RequestDetails requestDetails, + @OperationParam(name = "url") UriType url, + @OperationParam(name = "conceptMap") ConceptMap conceptMap, + @OperationParam(name = "conceptMapVersion") StringType conceptMapVersion, + @OperationParam(name = "code") CodeType code, + @OperationParam(name = "system") UriType system, + @OperationParam(name = "version") StringType version, + @OperationParam(name = "coding") Coding coding, + @OperationParam(name = "codeableConcept") CodeableConcept codeableConcept, + @OperationParam(name = "target") UriType target, + @OperationParam(name = "targetsystem") UriType targetSystem, + @OperationParam(name = "reverse") BooleanType reverse, + @OperationParam(name = "x-system-cache-id") StringType xSystemCacheId) { + + return service.translate( + url != null ? url.getValue() : null, + conceptMap, + conceptMapVersion != null ? conceptMapVersion.getValue() : null, + code != null ? code.getValue() : null, + system != null ? system.getValue() : null, + version != null ? version.getValue() : null, + coding, + codeableConcept, + target != null ? target.getValue() : null, + targetSystem != null ? targetSystem.getValue() : null, + reverse != null ? reverse.booleanValue() : null, + xSystemCacheId != null ? xSystemCacheId.getValue() : null); + } +} diff --git a/src/main/java/com/gointerop/fhir/repository/MappingRepository.java b/src/main/java/com/gointerop/fhir/repository/MappingRepository.java new file mode 100644 index 0000000..acb3a97 --- /dev/null +++ b/src/main/java/com/gointerop/fhir/repository/MappingRepository.java @@ -0,0 +1,80 @@ +package com.gointerop.fhir.repository; + +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Repository; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import com.gointerop.fhir.model.Concept; +import com.gointerop.fhir.model.Mapping; + +@Repository +public class MappingRepository { + + @Value("${ocl.base-url}") + private String oclBaseUrl; + + private final RestTemplate restTemplate; + + public MappingRepository(RestTemplateBuilder builder) { + this.restTemplate = builder + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(unsafeHttpClient())) + .build(); + } + + private CloseableHttpClient unsafeHttpClient() { + try { + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory( + sslContext, + NoopHostnameVerifier.INSTANCE); + HttpClientBuilder httpClientBuilder = HttpClients.custom(); + httpClientBuilder.setSSLSocketFactory(csf); + return httpClientBuilder.build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Cacheable(value = "mappings-by-org-and-source-and-concept", key = "#org + '-' + #source + '-' + #sourceConcept + '-' + #mapType") + public List mappingsByOrgAndSourceAndConcept(String org, String source, String sourceConcept) { + System.out.println("DEBUG: Fetching mappings by organization-source-concept: " + org + ", sourceId: " + source + ", concept: " + sourceConcept); + String url = String.format("%s/orgs/%s/sources/%s/concepts/%s/mappings", oclBaseUrl, org, source, sourceConcept); + ResponseEntity response = restTemplate.getForEntity(url, Mapping[].class); + return Arrays.asList(Objects.requireNonNull(response.getBody())); + + } +} diff --git a/src/main/java/com/gointerop/fhir/service/MappingService.java b/src/main/java/com/gointerop/fhir/service/MappingService.java new file mode 100644 index 0000000..9c9c89f --- /dev/null +++ b/src/main/java/com/gointerop/fhir/service/MappingService.java @@ -0,0 +1,192 @@ +package com.gointerop.fhir.service; + +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.hl7.fhir.r4.model.BaseResource; +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.OperationOutcome; +import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; +import org.hl7.fhir.r4.model.OperationOutcome.IssueType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import com.gointerop.fhir.converter.MappingParametersConverter; +import com.gointerop.fhir.helper.OperationOutcomeHelper; +import com.gointerop.fhir.model.Concept; +import com.gointerop.fhir.model.Mapping; +import com.gointerop.fhir.model.Source; +import com.gointerop.fhir.repository.ConceptRepository; +import com.gointerop.fhir.repository.MappingRepository; +import com.gointerop.fhir.repository.SourceRepository; + +@Service +public class MappingService { + + @Value("${ocl.base-url}") + private String oclBaseUrl; + + @Autowired + private SourceRepository sourceRepository; + + @Autowired + private ConceptRepository conceptRepository; + + @Autowired + private MappingRepository repository; + + @Autowired + private MappingParametersConverter converter; + + @Autowired + OperationOutcomeHelper operationOutcomeHelper; + + private CloseableHttpClient unsafeHttpClient() { + try { + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory( + sslContext, + NoopHostnameVerifier.INSTANCE); + HttpClientBuilder httpClientBuilder = HttpClients.custom(); + httpClientBuilder.setSSLSocketFactory(csf); + return httpClientBuilder.build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Cacheable(value = "translateCache", keyGenerator = "translateKeyGenerator") + public BaseResource translate( + String url, + ConceptMap conceptMap, + String conceptMapVersion, + String code, + String system, + String version, + Coding coding, + CodeableConcept codeableConcept, + String target, + String targetSystem, + Boolean reverse, + String xSystemCacheId) { + + if (system == null || system.isEmpty()) { + return buildError("In the current implementation, system parameter is required for translation."); + } + + if ((code == null || code.isEmpty()) && (coding == null) && (codeableConcept == null)) { + return buildError("code, coding or codeableConcept parameter are required for translation."); + } + + String codeToBeMatched = + code != null ? code : + (coding != null ? coding.getCode() : + (codeableConcept != null && !codeableConcept.getCoding().isEmpty() ? + codeableConcept.getCodingFirstRep().getCode() : null)); + + List sources = sourceRepository.fetchSourcesByCanonicalUrl(system); + + Source matchedSource = sources.stream() + .filter(source -> source.getCanonicalUrl() != null + && source.getCanonicalUrl().equalsIgnoreCase(system)) + .findFirst() + .orElse(null); + + if (matchedSource == null) { + return buildError("No CodeSystem found for system URL '" + system + "'"); + } + + Concept concept = null; + + try { + concept = conceptRepository.fetchConceptBySource( + matchedSource.getOwner(), + matchedSource.getId(), + codeToBeMatched); + } catch (Exception e) { + return buildError("Code '" + codeToBeMatched + "' not found in system '" + system + "'."); + } + + List mappings = repository.mappingsByOrgAndSourceAndConcept( + matchedSource.getOwner(), + matchedSource.getId(), + concept.getId()); + + if (mappings == null || mappings.isEmpty()) { + return buildError("No mapping found for concept '" + concept.getId() + "' in source '" + matchedSource.getId() + "'."); + } + + List filteredMappings = new ArrayList<>(); + List targetSources = new ArrayList<>(); + for (Mapping map : mappings) { + Source targetSource = sourceRepository.fetchSourceByOwnerAndId(map.getToSourceOwner(), + toSourceId(map.getToSourceUrl())); + + String targetCanonical = targetSource != null ? targetSource.getCanonicalUrl() : map.getToSourceUrl(); + + if (targetSystem != null && !targetSystem.isEmpty()) { + if (targetCanonical == null || !targetSystem.equalsIgnoreCase(targetCanonical)) { + continue; + } + } + + if (targetSource == null) { + continue; + } + + filteredMappings.add(map); + targetSources.add(targetSource); + } + + if (filteredMappings.isEmpty()) { + if (targetSystem != null && !targetSystem.isEmpty()) { + return buildError("No mapping found for concept '" + concept.getId() + "' in source '" + matchedSource.getId() + + "' matching target system '" + targetSystem + "'."); + } + return buildError("No mapping found for concept '" + concept.getId() + "' in source '" + matchedSource.getId() + "'."); + } + + return converter.toParameters(filteredMappings, targetSources); + } + + private OperationOutcome buildError(String details) { + return operationOutcomeHelper.help(IssueSeverity.ERROR,IssueType.INVARIANT, details); + } + + private String toSourceId(String sourceAccessionUrl) { + if (sourceAccessionUrl == null || sourceAccessionUrl.isEmpty()) { + return null; + } + String[] parts = sourceAccessionUrl.split("/"); + return parts[parts.length - 1]; + } +}