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.IBaseResource;
040import org.hl7.fhir.instance.model.api.IPrimitiveType;
041
042import java.util.HashMap;
043import java.util.List;
044import java.util.Map;
045import java.util.Objects;
046
047import static ca.uhn.fhir.rest.server.interceptor.InterceptorOrders.RESPONSE_TERMINOLOGY_TRANSLATION_INTERCEPTOR;
048import static org.apache.commons.lang3.StringUtils.isNotBlank;
049
050/**
051 * This interceptor leverages ConceptMap resources stored in the repository to automatically map
052 * terminology from one CodeSystem to another at runtime, in resources that are being
053 * returned by the server.
054 * <p>
055 * Mappings are applied only if they are explicitly configured in the interceptor via
056 * the {@link #addMappingSpecification(String, String)} method.
057 * </p>
058 *
059 * @since 5.4.0
060 */
061public class ResponseTerminologyTranslationInterceptor extends BaseResponseTerminologyInterceptor {
062
063        private final BaseRuntimeChildDefinition myCodingSystemChild;
064        private final BaseRuntimeChildDefinition myCodingCodeChild;
065        private final BaseRuntimeElementDefinition<IPrimitiveType<?>> myUriDefinition;
066        private final BaseRuntimeElementDefinition<IPrimitiveType<?>> myCodeDefinition;
067        private final Class<? extends IBase> myCodeableConceptType;
068        private final Class<? extends IBase> myCodingType;
069        private final BaseRuntimeChildDefinition myCodeableConceptCodingChild;
070        private final BaseRuntimeElementCompositeDefinition<?> myCodingDefinitition;
071        private final RuntimePrimitiveDatatypeDefinition myStringDefinition;
072        private final BaseRuntimeChildDefinition myCodingDisplayChild;
073        private Map<String, String> myMappingSpecifications = new HashMap<>();
074
075        /**
076         * Constructor
077         *
078         * @param theValidationSupport The validation support module
079         */
080        public ResponseTerminologyTranslationInterceptor(IValidationSupport theValidationSupport) {
081                super(theValidationSupport);
082
083                BaseRuntimeElementCompositeDefinition<?> codeableConceptDef = (BaseRuntimeElementCompositeDefinition<?>) Objects.requireNonNull(myContext.getElementDefinition("CodeableConcept"));
084                myCodeableConceptType = codeableConceptDef.getImplementingClass();
085                myCodeableConceptCodingChild = codeableConceptDef.getChildByName("coding");
086
087                myCodingDefinitition = (BaseRuntimeElementCompositeDefinition<?>) Objects.requireNonNull(myContext.getElementDefinition("Coding"));
088                myCodingType = myCodingDefinitition.getImplementingClass();
089                myCodingSystemChild = myCodingDefinitition.getChildByName("system");
090                myCodingCodeChild = myCodingDefinitition.getChildByName("code");
091                myCodingDisplayChild = myCodingDefinitition.getChildByName("display");
092
093                myUriDefinition = (RuntimePrimitiveDatatypeDefinition) myContext.getElementDefinition("uri");
094                myCodeDefinition = (RuntimePrimitiveDatatypeDefinition) myContext.getElementDefinition("code");
095                myStringDefinition = (RuntimePrimitiveDatatypeDefinition) myContext.getElementDefinition("string");
096        }
097
098        /**
099         * Adds a mapping specification using only a source and target CodeSystem URL. Any mappings specified using
100         * this URL
101         *
102         * @param theSourceCodeSystemUrl The source CodeSystem URL
103         * @param theTargetCodeSystemUrl The target CodeSystem URL
104         */
105        public void addMappingSpecification(String theSourceCodeSystemUrl, String theTargetCodeSystemUrl) {
106                Validate.notBlank(theSourceCodeSystemUrl, "theSourceCodeSystemUrl must not be null or blank");
107                Validate.notBlank(theTargetCodeSystemUrl, "theTargetCodeSystemUrl must not be null or blank");
108
109                myMappingSpecifications.put(theSourceCodeSystemUrl, theTargetCodeSystemUrl);
110        }
111
112        /**
113         * Clear all mapping specifications
114         */
115        public void clearMappingSpecifications() {
116                myMappingSpecifications.clear();
117        }
118
119
120        @Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE, order = RESPONSE_TERMINOLOGY_TRANSLATION_INTERCEPTOR)
121        public void handleResource(RequestDetails theRequestDetails, IBaseResource theResource) {
122                List<IBaseResource> resources = toListForProcessing(theRequestDetails, theResource);
123
124                FhirTerser terser = myContext.newTerser();
125                for (IBaseResource nextResource : resources) {
126                        terser.visit(nextResource, new MappingVisitor());
127                }
128
129        }
130
131
132        private class MappingVisitor implements IModelVisitor {
133
134                @Override
135                public void acceptElement(IBaseResource theResource, IBase theElement, List<String> thePathToElement, BaseRuntimeChildDefinition theChildDefinition, BaseRuntimeElementDefinition<?> theDefinition) {
136                        if (myCodeableConceptType.isAssignableFrom(theElement.getClass())) {
137
138                                // Find all existing Codings
139                                Multimap<String, String> foundSystemsToCodes = ArrayListMultimap.create();
140                                List<IBase> nextCodeableConceptCodings = myCodeableConceptCodingChild.getAccessor().getValues(theElement);
141                                for (IBase nextCodeableConceptCoding : nextCodeableConceptCodings) {
142                                        String system = myCodingSystemChild.getAccessor().getFirstValueOrNull(nextCodeableConceptCoding).map(t -> (IPrimitiveType<?>) t).map(t -> t.getValueAsString()).orElse(null);
143                                        String code = myCodingCodeChild.getAccessor().getFirstValueOrNull(nextCodeableConceptCoding).map(t -> (IPrimitiveType<?>) t).map(t -> t.getValueAsString()).orElse(null);
144                                        if (isNotBlank(system) && isNotBlank(code) && !foundSystemsToCodes.containsKey(system)) {
145                                                foundSystemsToCodes.put(system, code);
146                                        }
147                                }
148
149                                // Look for mappings
150                                for (String nextSourceSystem : foundSystemsToCodes.keySet()) {
151                                        String wantTargetSystem = myMappingSpecifications.get(nextSourceSystem);
152                                        if (wantTargetSystem != null) {
153                                                if (!foundSystemsToCodes.containsKey(wantTargetSystem)) {
154
155                                                        for (String code : foundSystemsToCodes.get(nextSourceSystem)) {
156                                                                TranslateConceptResults translateConceptResults = myValidationSupport.translateConcept(new IValidationSupport.TranslateCodeRequest(nextSourceSystem, code, wantTargetSystem));
157                                                                if (translateConceptResults != null) {
158                                                                        List<TranslateConceptResult> mappings = translateConceptResults.getResults();
159                                                                        for (TranslateConceptResult nextMapping : mappings) {
160
161                                                                                IBase newCoding = createCodingFromMappingTarget(nextMapping);
162
163                                                                                // Add coding to existing CodeableConcept
164                                                                                myCodeableConceptCodingChild.getMutator().addValue(theElement, newCoding);
165
166                                                                        }
167                                                                }
168                                                        }
169                                                }
170                                        }
171                                }
172
173                        }
174
175                }
176
177                private IBase createCodingFromMappingTarget(TranslateConceptResult nextMapping) {
178                        IBase newCoding = myCodingDefinitition.newInstance();
179                        IPrimitiveType<?> newSystem = myUriDefinition.newInstance(nextMapping.getSystem());
180                        myCodingSystemChild.getMutator().addValue(newCoding, newSystem);
181                        IPrimitiveType<?> newCode = myCodeDefinition.newInstance(nextMapping.getCode());
182                        myCodingCodeChild.getMutator().addValue(newCoding, newCode);
183                        if (isNotBlank(nextMapping.getDisplay())) {
184                                IPrimitiveType<?> newDisplay = myStringDefinition.newInstance(nextMapping.getDisplay());
185                                myCodingDisplayChild.getMutator().addValue(newCoding, newDisplay);
186                        }
187                        return newCoding;
188                }
189
190        }
191}