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}