001package ca.uhn.fhir.rest.server.interceptor; 002 003/*- 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 025import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 026import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition; 027import ca.uhn.fhir.context.support.IValidationSupport; 028import ca.uhn.fhir.context.support.TranslateConceptResult; 029import ca.uhn.fhir.context.support.TranslateConceptResults; 030import ca.uhn.fhir.interceptor.api.Hook; 031import ca.uhn.fhir.interceptor.api.Pointcut; 032import ca.uhn.fhir.rest.api.server.RequestDetails; 033import ca.uhn.fhir.util.FhirTerser; 034import ca.uhn.fhir.util.IModelVisitor; 035import com.google.common.collect.ArrayListMultimap; 036import com.google.common.collect.Multimap; 037import org.apache.commons.lang3.Validate; 038import org.hl7.fhir.instance.model.api.IBase; 039import org.hl7.fhir.instance.model.api.IBaseCoding; 040import org.hl7.fhir.instance.model.api.IBaseResource; 041import org.hl7.fhir.instance.model.api.IPrimitiveType; 042 043import java.util.ArrayList; 044import java.util.HashMap; 045import java.util.List; 046import java.util.Map; 047import java.util.Objects; 048 049import static ca.uhn.fhir.rest.server.interceptor.InterceptorOrders.RESPONSE_TERMINOLOGY_TRANSLATION_INTERCEPTOR; 050import static org.apache.commons.lang3.StringUtils.isNotBlank; 051 052/** 053 * This interceptor leverages ConceptMap resources stored in the repository to automatically map 054 * terminology from one CodeSystem to another at runtime, in resources that are being 055 * returned by the server. 056 * <p> 057 * Mappings are applied only if they are explicitly configured in the interceptor via 058 * the {@link #addMappingSpecification(String, String)} method. 059 * </p> 060 * 061 * @since 5.4.0 062 */ 063public class ResponseTerminologyTranslationInterceptor extends BaseResponseTerminologyInterceptor { 064 065 private final BaseRuntimeChildDefinition myCodingSystemChild; 066 private final BaseRuntimeChildDefinition myCodingCodeChild; 067 private final BaseRuntimeElementDefinition<IPrimitiveType<?>> myUriDefinition; 068 private final BaseRuntimeElementDefinition<IPrimitiveType<?>> myCodeDefinition; 069 private final Class<? extends IBase> myCodeableConceptType; 070 private final Class<? extends IBase> myCodingType; 071 private final BaseRuntimeChildDefinition myCodeableConceptCodingChild; 072 private final BaseRuntimeElementCompositeDefinition<?> myCodingDefinitition; 073 private final RuntimePrimitiveDatatypeDefinition myStringDefinition; 074 private final BaseRuntimeChildDefinition myCodingDisplayChild; 075 private Map<String, String> myMappingSpecifications = new HashMap<>(); 076 077 /** 078 * Constructor 079 * 080 * @param theValidationSupport The validation support module 081 */ 082 public ResponseTerminologyTranslationInterceptor(IValidationSupport theValidationSupport) { 083 super(theValidationSupport); 084 085 BaseRuntimeElementCompositeDefinition<?> codeableConceptDef = (BaseRuntimeElementCompositeDefinition<?>) Objects.requireNonNull(myContext.getElementDefinition("CodeableConcept")); 086 myCodeableConceptType = codeableConceptDef.getImplementingClass(); 087 myCodeableConceptCodingChild = codeableConceptDef.getChildByName("coding"); 088 089 myCodingDefinitition = (BaseRuntimeElementCompositeDefinition<?>) Objects.requireNonNull(myContext.getElementDefinition("Coding")); 090 myCodingType = myCodingDefinitition.getImplementingClass(); 091 myCodingSystemChild = myCodingDefinitition.getChildByName("system"); 092 myCodingCodeChild = myCodingDefinitition.getChildByName("code"); 093 myCodingDisplayChild = myCodingDefinitition.getChildByName("display"); 094 095 myUriDefinition = (RuntimePrimitiveDatatypeDefinition) myContext.getElementDefinition("uri"); 096 myCodeDefinition = (RuntimePrimitiveDatatypeDefinition) myContext.getElementDefinition("code"); 097 myStringDefinition = (RuntimePrimitiveDatatypeDefinition) myContext.getElementDefinition("string"); 098 } 099 100 /** 101 * Adds a mapping specification using only a source and target CodeSystem URL. Any mappings specified using 102 * this URL 103 * 104 * @param theSourceCodeSystemUrl The source CodeSystem URL 105 * @param theTargetCodeSystemUrl The target CodeSystem URL 106 */ 107 public void addMappingSpecification(String theSourceCodeSystemUrl, String theTargetCodeSystemUrl) { 108 Validate.notBlank(theSourceCodeSystemUrl, "theSourceCodeSystemUrl must not be null or blank"); 109 Validate.notBlank(theTargetCodeSystemUrl, "theTargetCodeSystemUrl must not be null or blank"); 110 111 myMappingSpecifications.put(theSourceCodeSystemUrl, theTargetCodeSystemUrl); 112 } 113 114 /** 115 * Clear all mapping specifications 116 */ 117 public void clearMappingSpecifications() { 118 myMappingSpecifications.clear(); 119 } 120 121 122 @Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE, order = RESPONSE_TERMINOLOGY_TRANSLATION_INTERCEPTOR) 123 public void handleResource(RequestDetails theRequestDetails, IBaseResource theResource) { 124 List<IBaseResource> resources = toListForProcessing(theRequestDetails, theResource); 125 126 FhirTerser terser = myContext.newTerser(); 127 for (IBaseResource nextResource : resources) { 128 terser.visit(nextResource, new MappingVisitor()); 129 } 130 131 } 132 133 134 private class MappingVisitor implements IModelVisitor { 135 136 @Override 137 public void acceptElement(IBaseResource theResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition) { 138 if (myCodeableConceptType.isAssignableFrom(theElement.getClass())) { 139 140 // Find all existing Codings 141 Multimap<String, String> foundSystemsToCodes = ArrayListMultimap.create(); 142 List<IBase> nextCodeableConceptCodings = myCodeableConceptCodingChild.getAccessor().getValues(theElement); 143 for (IBase nextCodeableConceptCoding : nextCodeableConceptCodings) { 144 String system = myCodingSystemChild.getAccessor().getFirstValueOrNull(nextCodeableConceptCoding).map(t -> (IPrimitiveType<?>) t).map(t -> t.getValueAsString()).orElse(null); 145 String code = myCodingCodeChild.getAccessor().getFirstValueOrNull(nextCodeableConceptCoding).map(t -> (IPrimitiveType<?>) t).map(t -> t.getValueAsString()).orElse(null); 146 if (isNotBlank(system) && isNotBlank(code) && !foundSystemsToCodes.containsKey(system)) { 147 foundSystemsToCodes.put(system, code); 148 } 149 } 150 151 // Look for mappings 152 for (String nextSourceSystem : foundSystemsToCodes.keySet()) { 153 String wantTargetSystem = myMappingSpecifications.get(nextSourceSystem); 154 if (wantTargetSystem != null) { 155 if (!foundSystemsToCodes.containsKey(wantTargetSystem)) { 156 157 for (String code : foundSystemsToCodes.get(nextSourceSystem)) { 158 List<IBaseCoding> codings = new ArrayList<IBaseCoding>(); 159 codings.add(createCodingFromPrimitives(nextSourceSystem, code, null)); 160 TranslateConceptResults translateConceptResults = myValidationSupport.translateConcept(new IValidationSupport.TranslateCodeRequest(codings, wantTargetSystem)); 161 if (translateConceptResults != null) { 162 List<TranslateConceptResult> mappings = translateConceptResults.getResults(); 163 for (TranslateConceptResult nextMapping : mappings) { 164 165 IBase newCoding = createCodingFromPrimitives( 166 nextMapping.getSystem(), 167 nextMapping.getCode(), 168 nextMapping.getDisplay()); 169 170 // Add coding to existing CodeableConcept 171 myCodeableConceptCodingChild.getMutator().addValue(theElement, newCoding); 172 173 } 174 } 175 } 176 } 177 } 178 } 179 180 } 181 182 } 183 184 private IBaseCoding createCodingFromPrimitives(String system, String code, String display) { 185 IBaseCoding newCoding = (IBaseCoding) myCodingDefinitition.newInstance(); 186 IPrimitiveType<?> newSystem = myUriDefinition.newInstance(system); 187 myCodingSystemChild.getMutator().addValue(newCoding, newSystem); 188 IPrimitiveType<?> newCode = myCodeDefinition.newInstance(code); 189 myCodingCodeChild.getMutator().addValue(newCoding, newCode); 190 if (isNotBlank(display)) { 191 IPrimitiveType<?> newDisplay = myStringDefinition.newInstance(display); 192 myCodingDisplayChild.getMutator().addValue(newCoding, newDisplay); 193 } 194 return newCoding; 195 } 196 197 } 198}