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