001/*-
002 * #%L
003 * HAPI FHIR JPA Server
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.jpa.term;
021
022import ca.uhn.fhir.context.support.TranslateConceptResult;
023import ca.uhn.fhir.context.support.TranslateConceptResults;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.jpa.api.model.TranslationRequest;
026import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupDao;
027import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementDao;
028import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao;
029import ca.uhn.fhir.jpa.entity.TermConceptMap;
030import ca.uhn.fhir.jpa.entity.TermConceptMapGroup;
031import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
032import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget;
033import ca.uhn.fhir.jpa.model.entity.ResourceTable;
034import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
035import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
036import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
037import ca.uhn.fhir.util.ValidateUtil;
038import com.google.common.annotations.VisibleForTesting;
039import org.hl7.fhir.exceptions.FHIRException;
040import org.hl7.fhir.r4.model.BooleanType;
041import org.hl7.fhir.r4.model.CodeType;
042import org.hl7.fhir.r4.model.Coding;
043import org.hl7.fhir.r4.model.ConceptMap;
044import org.hl7.fhir.r4.model.Enumerations;
045import org.hl7.fhir.r4.model.IdType;
046import org.hl7.fhir.r4.model.Parameters;
047import org.hl7.fhir.r4.model.StringType;
048import org.hl7.fhir.r4.model.UriType;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051import org.springframework.beans.factory.annotation.Autowired;
052import org.springframework.transaction.annotation.Transactional;
053
054import java.util.Optional;
055
056import static ca.uhn.fhir.jpa.term.TermReadSvcImpl.isPlaceholder;
057import static org.apache.commons.lang3.StringUtils.isBlank;
058import static org.apache.commons.lang3.StringUtils.isNotBlank;
059
060public class TermConceptMappingSvcImpl extends TermConceptClientMappingSvcImpl implements ITermConceptMappingSvc {
061
062        private static final Logger ourLog = LoggerFactory.getLogger(TermConceptMappingSvcImpl.class);
063
064        @Autowired
065        protected ITermConceptMapGroupDao myConceptMapGroupDao;
066
067        @Autowired
068        protected ITermConceptMapGroupElementDao myConceptMapGroupElementDao;
069
070        @Autowired
071        protected ITermConceptMapGroupElementTargetDao myConceptMapGroupElementTargetDao;
072
073        @Override
074        public String getName() {
075                return getFhirContext().getVersion().getVersion() + " ConceptMap Validation Support";
076        }
077
078        @Override
079        @Transactional
080        public void deleteConceptMapAndChildren(ResourceTable theResourceTable) {
081                deleteConceptMap(theResourceTable);
082        }
083
084        @Override
085        @Transactional
086        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
087                TranslationRequest request = TranslationRequest.fromTranslateCodeRequest(theRequest);
088                if (request.hasReverse() && request.getReverseAsBoolean()) {
089                        return translateWithReverse(request);
090                }
091
092                return translate(request);
093        }
094
095        @Override
096        @Transactional
097        public void storeTermConceptMapAndChildren(ResourceTable theResourceTable, ConceptMap theConceptMap) {
098
099                ValidateUtil.isTrueOrThrowInvalidRequest(theResourceTable != null, "No resource supplied");
100                if (isPlaceholder(theConceptMap)) {
101                        ourLog.info(
102                                        "Not storing TermConceptMap for placeholder {}",
103                                        theConceptMap.getIdElement().toVersionless().getValueAsString());
104                        return;
105                }
106
107                ValidateUtil.isNotBlankOrThrowUnprocessableEntity(
108                                theConceptMap.getUrl(), "ConceptMap has no value for ConceptMap.url");
109                ourLog.info(
110                                "Storing TermConceptMap for {}",
111                                theConceptMap.getIdElement().toVersionless().getValueAsString());
112
113                TermConceptMap termConceptMap = new TermConceptMap();
114                termConceptMap.setResource(theResourceTable);
115                termConceptMap.setUrl(theConceptMap.getUrl());
116                termConceptMap.setVersion(theConceptMap.getVersion());
117
118                String source = theConceptMap.hasSourceUriType()
119                                ? theConceptMap.getSourceUriType().getValueAsString()
120                                : null;
121                String target = theConceptMap.hasTargetUriType()
122                                ? theConceptMap.getTargetUriType().getValueAsString()
123                                : null;
124
125                /*
126                 * If this is a mapping between "resources" instead of purely between
127                 * "concepts" (this is a weird concept that is technically possible, at least as of
128                 * FHIR R4), don't try to store the mappings.
129                 *
130                 * See here for a description of what that is:
131                 * http://hl7.org/fhir/conceptmap.html#bnr
132                 */
133                if ("StructureDefinition".equals(new IdType(source).getResourceType())
134                                || "StructureDefinition".equals(new IdType(target).getResourceType())) {
135                        return;
136                }
137
138                if (source == null && theConceptMap.hasSourceCanonicalType()) {
139                        source = theConceptMap.getSourceCanonicalType().getValueAsString();
140                }
141                if (target == null && theConceptMap.hasTargetCanonicalType()) {
142                        target = theConceptMap.getTargetCanonicalType().getValueAsString();
143                }
144
145                /*
146                 * For now we always delete old versions. At some point, it would be nice to allow configuration to keep old versions.
147                 */
148                deleteConceptMap(theResourceTable);
149
150                /*
151                 * Do the upload.
152                 */
153                String conceptMapUrl = termConceptMap.getUrl();
154                String conceptMapVersion = termConceptMap.getVersion();
155                Optional<TermConceptMap> optionalExistingTermConceptMapByUrl;
156                if (isBlank(conceptMapVersion)) {
157                        optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndNullVersion(conceptMapUrl);
158                } else {
159                        optionalExistingTermConceptMapByUrl =
160                                        myConceptMapDao.findTermConceptMapByUrlAndVersion(conceptMapUrl, conceptMapVersion);
161                }
162                if (optionalExistingTermConceptMapByUrl.isEmpty()) {
163                        try {
164                                if (isNotBlank(source)) {
165                                        termConceptMap.setSource(source);
166                                }
167                                if (isNotBlank(target)) {
168                                        termConceptMap.setTarget(target);
169                                }
170                        } catch (FHIRException fe) {
171                                throw new InternalErrorException(Msg.code(837) + fe);
172                        }
173                        termConceptMap = myConceptMapDao.save(termConceptMap);
174                        int codesSaved = 0;
175
176                        TermConceptMapGroup termConceptMapGroup;
177                        for (ConceptMap.ConceptMapGroupComponent group : theConceptMap.getGroup()) {
178
179                                String groupSource = group.getSource();
180                                if (isBlank(groupSource)) {
181                                        groupSource = source;
182                                }
183                                if (isBlank(groupSource)) {
184                                        throw new UnprocessableEntityException(Msg.code(838) + "ConceptMap[url='" + theConceptMap.getUrl()
185                                                        + "'] contains at least one group without a value in ConceptMap.group.source");
186                                }
187
188                                String groupTarget = group.getTarget();
189                                if (isBlank(groupTarget)) {
190                                        groupTarget = target;
191                                }
192                                if (isBlank(groupTarget)) {
193                                        throw new UnprocessableEntityException(Msg.code(839) + "ConceptMap[url='" + theConceptMap.getUrl()
194                                                        + "'] contains at least one group without a value in ConceptMap.group.target");
195                                }
196
197                                termConceptMapGroup = new TermConceptMapGroup();
198                                termConceptMapGroup.setConceptMap(termConceptMap);
199                                termConceptMapGroup.setSource(groupSource);
200                                termConceptMapGroup.setSourceVersion(group.getSourceVersion());
201                                termConceptMapGroup.setTarget(groupTarget);
202                                termConceptMapGroup.setTargetVersion(group.getTargetVersion());
203                                termConceptMap.getConceptMapGroups().add(termConceptMapGroup);
204                                termConceptMapGroup = myConceptMapGroupDao.save(termConceptMapGroup);
205
206                                if (group.hasElement()) {
207                                        TermConceptMapGroupElement termConceptMapGroupElement;
208                                        for (ConceptMap.SourceElementComponent element : group.getElement()) {
209                                                if (isBlank(element.getCode())) {
210                                                        continue;
211                                                }
212                                                termConceptMapGroupElement = new TermConceptMapGroupElement();
213                                                termConceptMapGroupElement.setConceptMapGroup(termConceptMapGroup);
214                                                termConceptMapGroupElement.setCode(element.getCode());
215                                                termConceptMapGroupElement.setDisplay(element.getDisplay());
216                                                termConceptMapGroup.getConceptMapGroupElements().add(termConceptMapGroupElement);
217                                                termConceptMapGroupElement = myConceptMapGroupElementDao.save(termConceptMapGroupElement);
218
219                                                if (element.hasTarget()) {
220                                                        TermConceptMapGroupElementTarget termConceptMapGroupElementTarget;
221                                                        for (ConceptMap.TargetElementComponent elementTarget : element.getTarget()) {
222                                                                if (isBlank(elementTarget.getCode())
223                                                                                && elementTarget.getEquivalence()
224                                                                                                != Enumerations.ConceptMapEquivalence.UNMATCHED) {
225                                                                        continue;
226                                                                }
227                                                                termConceptMapGroupElementTarget = new TermConceptMapGroupElementTarget();
228                                                                termConceptMapGroupElementTarget.setConceptMapGroupElement(termConceptMapGroupElement);
229                                                                if (isNotBlank(elementTarget.getCode())) {
230                                                                        termConceptMapGroupElementTarget.setCode(elementTarget.getCode());
231                                                                        termConceptMapGroupElementTarget.setDisplay(elementTarget.getDisplay());
232                                                                }
233                                                                termConceptMapGroupElementTarget.setEquivalence(elementTarget.getEquivalence());
234                                                                termConceptMapGroupElement
235                                                                                .getConceptMapGroupElementTargets()
236                                                                                .add(termConceptMapGroupElementTarget);
237                                                                myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget);
238
239                                                                if (++codesSaved % 250 == 0) {
240                                                                        ourLog.info("Have saved {} codes in ConceptMap", codesSaved);
241                                                                        myConceptMapGroupElementTargetDao.flush();
242                                                                }
243                                                        }
244                                                }
245                                        }
246                                }
247                        }
248
249                } else {
250                        TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapByUrl.get();
251
252                        if (isBlank(conceptMapVersion)) {
253                                String msg = myContext
254                                                .getLocalizer()
255                                                .getMessage(
256                                                                TermReadSvcImpl.class,
257                                                                "cannotCreateDuplicateConceptMapUrl",
258                                                                conceptMapUrl,
259                                                                existingTermConceptMap
260                                                                                .getResource()
261                                                                                .getIdDt()
262                                                                                .toUnqualifiedVersionless()
263                                                                                .getValue());
264                                throw new UnprocessableEntityException(Msg.code(840) + msg);
265
266                        } else {
267                                String msg = myContext
268                                                .getLocalizer()
269                                                .getMessage(
270                                                                TermReadSvcImpl.class,
271                                                                "cannotCreateDuplicateConceptMapUrlAndVersion",
272                                                                conceptMapUrl,
273                                                                conceptMapVersion,
274                                                                existingTermConceptMap
275                                                                                .getResource()
276                                                                                .getIdDt()
277                                                                                .toUnqualifiedVersionless()
278                                                                                .getValue());
279                                throw new UnprocessableEntityException(Msg.code(841) + msg);
280                        }
281                }
282
283                ourLog.info(
284                                "Done storing TermConceptMap[{}] for {}",
285                                termConceptMap.getId(),
286                                theConceptMap.getIdElement().toVersionless().getValueAsString());
287        }
288
289        public void deleteConceptMap(ResourceTable theResourceTable) {
290                // Get existing entity so it can be deleted.
291                Optional<TermConceptMap> optionalExistingTermConceptMapById =
292                                myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId());
293
294                if (optionalExistingTermConceptMapById.isPresent()) {
295                        TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get();
296
297                        ourLog.info("Deleting existing TermConceptMap[{}] and its children...", existingTermConceptMap.getId());
298                        for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) {
299
300                                for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) {
301
302                                        for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) {
303
304                                                myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId());
305                                        }
306
307                                        myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId());
308                                }
309
310                                myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId());
311                        }
312
313                        myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId());
314                        ourLog.info("Done deleting existing TermConceptMap[{}] and its children.", existingTermConceptMap.getId());
315                }
316        }
317
318        /**
319         * This method is present only for unit tests, do not call from client code
320         */
321        @VisibleForTesting
322        public static void clearOurLastResultsFromTranslationCache() {
323                ourLastResultsFromTranslationCache = false;
324        }
325
326        /**
327         * This method is present only for unit tests, do not call from client code
328         */
329        @VisibleForTesting
330        public static void clearOurLastResultsFromTranslationWithReverseCache() {
331                ourLastResultsFromTranslationWithReverseCache = false;
332        }
333
334        /**
335         * This method is present only for unit tests, do not call from client code
336         */
337        @VisibleForTesting
338        static boolean isOurLastResultsFromTranslationCache() {
339                return ourLastResultsFromTranslationCache;
340        }
341
342        /**
343         * This method is present only for unit tests, do not call from client code
344         */
345        @VisibleForTesting
346        static boolean isOurLastResultsFromTranslationWithReverseCache() {
347                return ourLastResultsFromTranslationWithReverseCache;
348        }
349
350        public static Parameters toParameters(TranslateConceptResults theTranslationResult) {
351                Parameters retVal = new Parameters();
352
353                retVal.addParameter().setName("result").setValue(new BooleanType(theTranslationResult.getResult()));
354
355                if (theTranslationResult.getMessage() != null) {
356                        retVal.addParameter().setName("message").setValue(new StringType(theTranslationResult.getMessage()));
357                }
358
359                for (TranslateConceptResult translationMatch : theTranslationResult.getResults()) {
360                        Parameters.ParametersParameterComponent matchParam =
361                                        retVal.addParameter().setName("match");
362                        populateTranslateMatchParts(translationMatch, matchParam);
363                }
364
365                return retVal;
366        }
367
368        private static void populateTranslateMatchParts(
369                        TranslateConceptResult theTranslationMatch, Parameters.ParametersParameterComponent theParam) {
370                if (theTranslationMatch.getEquivalence() != null) {
371                        theParam.addPart().setName("equivalence").setValue(new CodeType(theTranslationMatch.getEquivalence()));
372                }
373
374                if (isNotBlank(theTranslationMatch.getSystem())
375                                || isNotBlank(theTranslationMatch.getCode())
376                                || isNotBlank(theTranslationMatch.getDisplay())) {
377                        Coding value = new Coding(
378                                        theTranslationMatch.getSystem(), theTranslationMatch.getCode(), theTranslationMatch.getDisplay());
379
380                        if (isNotBlank(theTranslationMatch.getSystemVersion())) {
381                                value.setVersion(theTranslationMatch.getSystemVersion());
382                        }
383
384                        theParam.addPart().setName("concept").setValue(value);
385                }
386
387                if (isNotBlank(theTranslationMatch.getConceptMapUrl())) {
388                        theParam.addPart().setName("source").setValue(new UriType(theTranslationMatch.getConceptMapUrl()));
389                }
390        }
391}