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 org.hl7.fhir.exceptions.FHIRException;
039import org.hl7.fhir.r4.model.BooleanType;
040import org.hl7.fhir.r4.model.CodeType;
041import org.hl7.fhir.r4.model.Coding;
042import org.hl7.fhir.r4.model.ConceptMap;
043import org.hl7.fhir.r4.model.Enumerations;
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                                                                                && elementTarget.getEquivalence()
223                                                                                                != Enumerations.ConceptMapEquivalence.UNMATCHED) {
224                                                                        continue;
225                                                                }
226                                                                termConceptMapGroupElementTarget = new TermConceptMapGroupElementTarget();
227                                                                termConceptMapGroupElementTarget.setConceptMapGroupElement(termConceptMapGroupElement);
228                                                                if (isNotBlank(elementTarget.getCode())) {
229                                                                        termConceptMapGroupElementTarget.setCode(elementTarget.getCode());
230                                                                        termConceptMapGroupElementTarget.setDisplay(elementTarget.getDisplay());
231                                                                }
232                                                                termConceptMapGroupElementTarget.setEquivalence(elementTarget.getEquivalence());
233                                                                termConceptMapGroupElement
234                                                                                .getConceptMapGroupElementTargets()
235                                                                                .add(termConceptMapGroupElementTarget);
236                                                                myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget);
237
238                                                                if (++codesSaved % 250 == 0) {
239                                                                        ourLog.info("Have saved {} codes in ConceptMap", codesSaved);
240                                                                        myConceptMapGroupElementTargetDao.flush();
241                                                                }
242                                                        }
243                                                }
244                                        }
245                                }
246                        }
247
248                } else {
249                        TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapByUrl.get();
250
251                        if (isBlank(conceptMapVersion)) {
252                                String msg = myContext
253                                                .getLocalizer()
254                                                .getMessage(
255                                                                TermReadSvcImpl.class,
256                                                                "cannotCreateDuplicateConceptMapUrl",
257                                                                conceptMapUrl,
258                                                                existingTermConceptMap
259                                                                                .getResource()
260                                                                                .getIdDt()
261                                                                                .toUnqualifiedVersionless()
262                                                                                .getValue());
263                                throw new UnprocessableEntityException(Msg.code(840) + msg);
264
265                        } else {
266                                String msg = myContext
267                                                .getLocalizer()
268                                                .getMessage(
269                                                                TermReadSvcImpl.class,
270                                                                "cannotCreateDuplicateConceptMapUrlAndVersion",
271                                                                conceptMapUrl,
272                                                                conceptMapVersion,
273                                                                existingTermConceptMap
274                                                                                .getResource()
275                                                                                .getIdDt()
276                                                                                .toUnqualifiedVersionless()
277                                                                                .getValue());
278                                throw new UnprocessableEntityException(Msg.code(841) + msg);
279                        }
280                }
281
282                ourLog.info(
283                                "Done storing TermConceptMap[{}] for {}",
284                                termConceptMap.getId(),
285                                theConceptMap.getIdElement().toVersionless().getValueAsString());
286        }
287
288        public void deleteConceptMap(ResourceTable theResourceTable) {
289                // Get existing entity so it can be deleted.
290                Optional<TermConceptMap> optionalExistingTermConceptMapById =
291                                myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId());
292
293                if (optionalExistingTermConceptMapById.isPresent()) {
294                        TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get();
295
296                        ourLog.info("Deleting existing TermConceptMap[{}] and its children...", existingTermConceptMap.getId());
297                        for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) {
298
299                                for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) {
300
301                                        for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) {
302
303                                                myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId());
304                                        }
305
306                                        myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId());
307                                }
308
309                                myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId());
310                        }
311
312                        myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId());
313                        ourLog.info("Done deleting existing TermConceptMap[{}] and its children.", existingTermConceptMap.getId());
314                }
315        }
316
317        public static Parameters toParameters(TranslateConceptResults theTranslationResult) {
318                Parameters retVal = new Parameters();
319
320                retVal.addParameter().setName("result").setValue(new BooleanType(theTranslationResult.getResult()));
321
322                if (theTranslationResult.getMessage() != null) {
323                        retVal.addParameter().setName("message").setValue(new StringType(theTranslationResult.getMessage()));
324                }
325
326                for (TranslateConceptResult translationMatch : theTranslationResult.getResults()) {
327                        Parameters.ParametersParameterComponent matchParam =
328                                        retVal.addParameter().setName("match");
329                        populateTranslateMatchParts(translationMatch, matchParam);
330                }
331
332                return retVal;
333        }
334
335        private static void populateTranslateMatchParts(
336                        TranslateConceptResult theTranslationMatch, Parameters.ParametersParameterComponent theParam) {
337                if (theTranslationMatch.getEquivalence() != null) {
338                        theParam.addPart().setName("equivalence").setValue(new CodeType(theTranslationMatch.getEquivalence()));
339                }
340
341                if (isNotBlank(theTranslationMatch.getSystem())
342                                || isNotBlank(theTranslationMatch.getCode())
343                                || isNotBlank(theTranslationMatch.getDisplay())) {
344                        Coding value = new Coding(
345                                        theTranslationMatch.getSystem(), theTranslationMatch.getCode(), theTranslationMatch.getDisplay());
346
347                        if (isNotBlank(theTranslationMatch.getSystemVersion())) {
348                                value.setVersion(theTranslationMatch.getSystemVersion());
349                        }
350
351                        theParam.addPart().setName("concept").setValue(value);
352                }
353
354                if (isNotBlank(theTranslationMatch.getConceptMapUrl())) {
355                        theParam.addPart().setName("source").setValue(new UriType(theTranslationMatch.getConceptMapUrl()));
356                }
357        }
358}