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