001package ca.uhn.fhir.jpa.term;
002
003/*-
004 * #%L
005 * HAPI FHIR JPA Server
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.i18n.Msg;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.context.support.TranslateConceptResult;
026import ca.uhn.fhir.context.support.TranslateConceptResults;
027import ca.uhn.fhir.jpa.api.model.TranslationQuery;
028import ca.uhn.fhir.jpa.api.model.TranslationRequest;
029import ca.uhn.fhir.jpa.dao.data.ITermConceptMapDao;
030import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupDao;
031import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementDao;
032import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao;
033import ca.uhn.fhir.jpa.entity.TermConceptMap;
034import ca.uhn.fhir.jpa.entity.TermConceptMapGroup;
035import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
036import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget;
037import ca.uhn.fhir.jpa.model.entity.ResourceTable;
038import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
039import ca.uhn.fhir.jpa.util.MemoryCacheService;
040import ca.uhn.fhir.jpa.util.ScrollableResultsIterator;
041import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
042import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
043import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
044import ca.uhn.fhir.util.ValidateUtil;
045import com.google.common.annotations.VisibleForTesting;
046import org.apache.commons.lang3.StringUtils;
047import org.hibernate.ScrollMode;
048import org.hibernate.ScrollableResults;
049import org.hl7.fhir.exceptions.FHIRException;
050import org.hl7.fhir.instance.model.api.IBase;
051import org.hl7.fhir.instance.model.api.IBaseCoding;
052import org.hl7.fhir.r4.model.BooleanType;
053import org.hl7.fhir.r4.model.CodeType;
054import org.hl7.fhir.r4.model.CodeableConcept;
055import org.hl7.fhir.r4.model.Coding;
056import org.hl7.fhir.r4.model.ConceptMap;
057import org.hl7.fhir.r4.model.IdType;
058import org.hl7.fhir.r4.model.Parameters;
059import org.hl7.fhir.r4.model.StringType;
060import org.hl7.fhir.r4.model.UriType;
061import org.slf4j.Logger;
062import org.slf4j.LoggerFactory;
063import org.springframework.beans.factory.annotation.Autowired;
064import org.springframework.data.domain.PageRequest;
065import org.springframework.data.domain.Pageable;
066import org.springframework.transaction.annotation.Propagation;
067import org.springframework.transaction.annotation.Transactional;
068
069import javax.persistence.EntityManager;
070import javax.persistence.PersistenceContext;
071import javax.persistence.PersistenceContextType;
072import javax.persistence.TypedQuery;
073import javax.persistence.criteria.CriteriaBuilder;
074import javax.persistence.criteria.CriteriaQuery;
075import javax.persistence.criteria.Join;
076import javax.persistence.criteria.Predicate;
077import javax.persistence.criteria.Root;
078import java.util.ArrayList;
079import java.util.HashSet;
080import java.util.List;
081import java.util.Optional;
082import java.util.Set;
083
084import static ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.isPlaceholder;
085import static org.apache.commons.lang3.StringUtils.isBlank;
086import static org.apache.commons.lang3.StringUtils.isNotBlank;
087
088public class TermConceptMappingSvcImpl implements ITermConceptMappingSvc {
089
090        private static final Logger ourLog = LoggerFactory.getLogger(TermConceptMappingSvcImpl.class);
091        private static boolean ourLastResultsFromTranslationCache; // For testing.
092        private static boolean ourLastResultsFromTranslationWithReverseCache; // For testing.
093        private final int myFetchSize = BaseTermReadSvcImpl.DEFAULT_FETCH_SIZE;
094        @Autowired
095        protected ITermConceptMapDao myConceptMapDao;
096        @Autowired
097        protected ITermConceptMapGroupDao myConceptMapGroupDao;
098        @Autowired
099        protected ITermConceptMapGroupElementDao myConceptMapGroupElementDao;
100        @Autowired
101        protected ITermConceptMapGroupElementTargetDao myConceptMapGroupElementTargetDao;
102        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
103        protected EntityManager myEntityManager;
104        @Autowired
105        private FhirContext myContext;
106        @Autowired
107        private MemoryCacheService myMemoryCacheService;
108
109        @Override
110        @Transactional
111        public void deleteConceptMapAndChildren(ResourceTable theResourceTable) {
112                deleteConceptMap(theResourceTable);
113        }
114
115        @Override
116        public FhirContext getFhirContext() {
117                return myContext;
118        }
119
120        @Override
121        @Transactional
122        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
123                TranslationRequest request = TranslationRequest.fromTranslateCodeRequest(theRequest);
124                if (request.hasReverse() && request.getReverseAsBoolean()) {
125                        return translateWithReverse(request);
126                }
127
128                return translate(request);
129        }
130
131        @Override
132        @Transactional
133        public void storeTermConceptMapAndChildren(ResourceTable theResourceTable, ConceptMap theConceptMap) {
134
135                ValidateUtil.isTrueOrThrowInvalidRequest(theResourceTable != null, "No resource supplied");
136                if (isPlaceholder(theConceptMap)) {
137                        ourLog.info("Not storing TermConceptMap for placeholder {}", theConceptMap.getIdElement().toVersionless().getValueAsString());
138                        return;
139                }
140
141                ValidateUtil.isNotBlankOrThrowUnprocessableEntity(theConceptMap.getUrl(), "ConceptMap has no value for ConceptMap.url");
142                ourLog.info("Storing TermConceptMap for {}", theConceptMap.getIdElement().toVersionless().getValueAsString());
143
144                TermConceptMap termConceptMap = new TermConceptMap();
145                termConceptMap.setResource(theResourceTable);
146                termConceptMap.setUrl(theConceptMap.getUrl());
147                termConceptMap.setVersion(theConceptMap.getVersion());
148
149                String source = theConceptMap.hasSourceUriType() ? theConceptMap.getSourceUriType().getValueAsString() : null;
150                String target = theConceptMap.hasTargetUriType() ? theConceptMap.getTargetUriType().getValueAsString() : null;
151
152                /*
153                 * If this is a mapping between "resources" instead of purely between
154                 * "concepts" (this is a weird concept that is technically possible, at least as of
155                 * FHIR R4), don't try to store the mappings.
156                 *
157                 * See here for a description of what that is:
158                 * http://hl7.org/fhir/conceptmap.html#bnr
159                 */
160                if ("StructureDefinition".equals(new IdType(source).getResourceType()) ||
161                        "StructureDefinition".equals(new IdType(target).getResourceType())) {
162                        return;
163                }
164
165                if (source == null && theConceptMap.hasSourceCanonicalType()) {
166                        source = theConceptMap.getSourceCanonicalType().getValueAsString();
167                }
168                if (target == null && theConceptMap.hasTargetCanonicalType()) {
169                        target = theConceptMap.getTargetCanonicalType().getValueAsString();
170                }
171
172                /*
173                 * For now we always delete old versions. At some point, it would be nice to allow configuration to keep old versions.
174                 */
175                deleteConceptMap(theResourceTable);
176
177                /*
178                 * Do the upload.
179                 */
180                String conceptMapUrl = termConceptMap.getUrl();
181                String conceptMapVersion = termConceptMap.getVersion();
182                Optional<TermConceptMap> optionalExistingTermConceptMapByUrl;
183                if (isBlank(conceptMapVersion)) {
184                        optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndNullVersion(conceptMapUrl);
185                } else {
186                        optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndVersion(conceptMapUrl, conceptMapVersion);
187                }
188                if (!optionalExistingTermConceptMapByUrl.isPresent()) {
189                        try {
190                                if (isNotBlank(source)) {
191                                        termConceptMap.setSource(source);
192                                }
193                                if (isNotBlank(target)) {
194                                        termConceptMap.setTarget(target);
195                                }
196                        } catch (FHIRException fe) {
197                                throw new InternalErrorException(Msg.code(837) + fe);
198                        }
199                        termConceptMap = myConceptMapDao.save(termConceptMap);
200                        int codesSaved = 0;
201
202                        TermConceptMapGroup termConceptMapGroup;
203                        for (ConceptMap.ConceptMapGroupComponent group : theConceptMap.getGroup()) {
204
205                                String groupSource = group.getSource();
206                                if (isBlank(groupSource)) {
207                                        groupSource = source;
208                                }
209                                if (isBlank(groupSource)) {
210                                        throw new UnprocessableEntityException(Msg.code(838) + "ConceptMap[url='" + theConceptMap.getUrl() + "'] contains at least one group without a value in ConceptMap.group.source");
211                                }
212
213                                String groupTarget = group.getTarget();
214                                if (isBlank(groupTarget)) {
215                                        groupTarget = target;
216                                }
217                                if (isBlank(groupTarget)) {
218                                        throw new UnprocessableEntityException(Msg.code(839) + "ConceptMap[url='" + theConceptMap.getUrl() + "'] contains at least one group without a value in ConceptMap.group.target");
219                                }
220
221                                termConceptMapGroup = new TermConceptMapGroup();
222                                termConceptMapGroup.setConceptMap(termConceptMap);
223                                termConceptMapGroup.setSource(groupSource);
224                                termConceptMapGroup.setSourceVersion(group.getSourceVersion());
225                                termConceptMapGroup.setTarget(groupTarget);
226                                termConceptMapGroup.setTargetVersion(group.getTargetVersion());
227                                termConceptMapGroup = myConceptMapGroupDao.save(termConceptMapGroup);
228
229                                if (group.hasElement()) {
230                                        TermConceptMapGroupElement termConceptMapGroupElement;
231                                        for (ConceptMap.SourceElementComponent element : group.getElement()) {
232                                                if (isBlank(element.getCode())) {
233                                                        continue;
234                                                }
235                                                termConceptMapGroupElement = new TermConceptMapGroupElement();
236                                                termConceptMapGroupElement.setConceptMapGroup(termConceptMapGroup);
237                                                termConceptMapGroupElement.setCode(element.getCode());
238                                                termConceptMapGroupElement.setDisplay(element.getDisplay());
239                                                termConceptMapGroupElement = myConceptMapGroupElementDao.save(termConceptMapGroupElement);
240
241                                                if (element.hasTarget()) {
242                                                        TermConceptMapGroupElementTarget termConceptMapGroupElementTarget;
243                                                        for (ConceptMap.TargetElementComponent elementTarget : element.getTarget()) {
244                                                                if (isBlank(elementTarget.getCode())) {
245                                                                        continue;
246                                                                }
247                                                                termConceptMapGroupElementTarget = new TermConceptMapGroupElementTarget();
248                                                                termConceptMapGroupElementTarget.setConceptMapGroupElement(termConceptMapGroupElement);
249                                                                termConceptMapGroupElementTarget.setCode(elementTarget.getCode());
250                                                                termConceptMapGroupElementTarget.setDisplay(elementTarget.getDisplay());
251                                                                termConceptMapGroupElementTarget.setEquivalence(elementTarget.getEquivalence());
252                                                                myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget);
253
254                                                                if (++codesSaved % 250 == 0) {
255                                                                        ourLog.info("Have saved {} codes in ConceptMap", codesSaved);
256                                                                        myConceptMapGroupElementTargetDao.flush();
257                                                                }
258                                                        }
259                                                }
260                                        }
261                                }
262                        }
263
264                } else {
265                        TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapByUrl.get();
266
267                        if (isBlank(conceptMapVersion)) {
268                                String msg = myContext.getLocalizer().getMessage(
269                                        BaseTermReadSvcImpl.class,
270                                        "cannotCreateDuplicateConceptMapUrl",
271                                        conceptMapUrl,
272                                        existingTermConceptMap.getResource().getIdDt().toUnqualifiedVersionless().getValue());
273                                throw new UnprocessableEntityException(Msg.code(840) + msg);
274
275                        } else {
276                                String msg = myContext.getLocalizer().getMessage(
277                                        BaseTermReadSvcImpl.class,
278                                        "cannotCreateDuplicateConceptMapUrlAndVersion",
279                                        conceptMapUrl, conceptMapVersion,
280                                        existingTermConceptMap.getResource().getIdDt().toUnqualifiedVersionless().getValue());
281                                throw new UnprocessableEntityException(Msg.code(841) + msg);
282                        }
283                }
284
285                ourLog.info("Done storing TermConceptMap[{}] for {}", termConceptMap.getId(), theConceptMap.getIdElement().toVersionless().getValueAsString());
286        }
287
288        @Override
289        @Transactional(propagation = Propagation.REQUIRED)
290        public TranslateConceptResults translate(TranslationRequest theTranslationRequest) {
291                TranslateConceptResults retVal = new TranslateConceptResults();
292
293                CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
294                CriteriaQuery<TermConceptMapGroupElementTarget> query = criteriaBuilder.createQuery(TermConceptMapGroupElementTarget.class);
295                Root<TermConceptMapGroupElementTarget> root = query.from(TermConceptMapGroupElementTarget.class);
296
297                Join<TermConceptMapGroupElementTarget, TermConceptMapGroupElement> elementJoin = root.join("myConceptMapGroupElement");
298                Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = elementJoin.join("myConceptMapGroup");
299                Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
300
301                List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
302                List<TranslateConceptResult> cachedTargets;
303                ArrayList<Predicate> predicates;
304                Coding coding;
305
306                //-- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version
307                String latestConceptMapVersion = null;
308                if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion())
309                        latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest);
310
311                for (TranslationQuery translationQuery : translationQueries) {
312                        cachedTargets = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION, translationQuery);
313                        if (cachedTargets == null) {
314                                final List<TranslateConceptResult> targets = new ArrayList<>();
315
316                                predicates = new ArrayList<>();
317
318                                coding = translationQuery.getCoding();
319                                if (coding.hasCode()) {
320                                        predicates.add(criteriaBuilder.equal(elementJoin.get("myCode"), coding.getCode()));
321                                } else {
322                                        throw new InvalidRequestException(Msg.code(842) + "A code must be provided for translation to occur.");
323                                }
324
325                                if (coding.hasSystem()) {
326                                        predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), coding.getSystem()));
327                                }
328
329                                if (coding.hasVersion()) {
330                                        predicates.add(criteriaBuilder.equal(groupJoin.get("mySourceVersion"), coding.getVersion()));
331                                }
332
333                                if (translationQuery.hasTargetSystem()) {
334                                        predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), translationQuery.getTargetSystem().getValueAsString()));
335                                }
336
337                                if (translationQuery.hasUrl()) {
338                                        predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl().getValueAsString()));
339                                        if (translationQuery.hasConceptMapVersion()) {
340                                                // both url and conceptMapVersion
341                                                predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion().getValueAsString()));
342                                        } else {
343                                                if (StringUtils.isNotBlank(latestConceptMapVersion)) {
344                                                        // only url and use latestConceptMapVersion
345                                                        predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion));
346                                                } else {
347                                                        predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion")));
348                                                }
349                                        }
350                                }
351
352                                if (translationQuery.hasSource()) {
353                                        predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getSource().getValueAsString()));
354                                }
355
356                                if (translationQuery.hasTarget()) {
357                                        predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getTarget().getValueAsString()));
358                                }
359
360                                if (translationQuery.hasResourceId()) {
361                                        predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), translationQuery.getResourceId()));
362                                }
363
364                                Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0]));
365                                query.where(outerPredicate);
366
367                                // Use scrollable results.
368                                final TypedQuery<TermConceptMapGroupElementTarget> typedQuery = myEntityManager.createQuery(query.select(root));
369                                org.hibernate.query.Query<TermConceptMapGroupElementTarget> hibernateQuery = (org.hibernate.query.Query<TermConceptMapGroupElementTarget>) typedQuery;
370                                hibernateQuery.setFetchSize(myFetchSize);
371                                ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
372                                try (ScrollableResultsIterator<TermConceptMapGroupElementTarget> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults)) {
373
374                                        Set<TermConceptMapGroupElementTarget> matches = new HashSet<>();
375                                        while (scrollableResultsIterator.hasNext()) {
376                                                TermConceptMapGroupElementTarget next = scrollableResultsIterator.next();
377                                                if (matches.add(next)) {
378
379                                                        TranslateConceptResult translationMatch = new TranslateConceptResult();
380                                                        if (next.getEquivalence() != null) {
381                                                                translationMatch.setEquivalence(next.getEquivalence().toCode());
382                                                        }
383
384                                                        translationMatch.setCode(next.getCode());
385                                                        translationMatch.setSystem(next.getSystem());
386                                                        translationMatch.setSystemVersion(next.getSystemVersion());
387                                                        translationMatch.setDisplay(next.getDisplay());
388                                                        translationMatch.setValueSet(next.getValueSet());
389                                                        translationMatch.setSystemVersion(next.getSystemVersion());
390                                                        translationMatch.setConceptMapUrl(next.getConceptMapUrl());
391
392                                                        targets.add(translationMatch);
393                                                }
394                                        }
395
396                                }
397
398                                ourLastResultsFromTranslationCache = false; // For testing.
399                                myMemoryCacheService.put(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION, translationQuery, targets);
400                                retVal.getResults().addAll(targets);
401                        } else {
402                                ourLastResultsFromTranslationCache = true; // For testing.
403                                retVal.getResults().addAll(cachedTargets);
404                        }
405                }
406
407                buildTranslationResult(retVal);
408                return retVal;
409        }
410
411        @Override
412        @Transactional(propagation = Propagation.REQUIRED)
413        public TranslateConceptResults translateWithReverse(TranslationRequest theTranslationRequest) {
414                TranslateConceptResults retVal = new TranslateConceptResults();
415
416                CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
417                CriteriaQuery<TermConceptMapGroupElement> query = criteriaBuilder.createQuery(TermConceptMapGroupElement.class);
418                Root<TermConceptMapGroupElement> root = query.from(TermConceptMapGroupElement.class);
419
420                Join<TermConceptMapGroupElement, TermConceptMapGroupElementTarget> targetJoin = root.join("myConceptMapGroupElementTargets");
421                Join<TermConceptMapGroupElement, TermConceptMapGroup> groupJoin = root.join("myConceptMapGroup");
422                Join<TermConceptMapGroup, TermConceptMap> conceptMapJoin = groupJoin.join("myConceptMap");
423
424                List<TranslationQuery> translationQueries = theTranslationRequest.getTranslationQueries();
425                List<TranslateConceptResult> cachedElements;
426                ArrayList<Predicate> predicates;
427                Coding coding;
428
429                //-- get the latest ConceptMapVersion if theTranslationRequest has ConceptMap url but no ConceptMap version
430                String latestConceptMapVersion = null;
431                if (theTranslationRequest.hasUrl() && !theTranslationRequest.hasConceptMapVersion())
432                        latestConceptMapVersion = getLatestConceptMapVersion(theTranslationRequest);
433
434                for (TranslationQuery translationQuery : translationQueries) {
435                        cachedElements = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION_REVERSE, translationQuery);
436                        if (cachedElements == null) {
437                                final List<TranslateConceptResult> elements = new ArrayList<>();
438
439                                predicates = new ArrayList<>();
440
441                                coding = translationQuery.getCoding();
442                                String targetCode;
443                                String targetCodeSystem = null;
444                                if (coding.hasCode()) {
445                                        predicates.add(criteriaBuilder.equal(targetJoin.get("myCode"), coding.getCode()));
446                                        targetCode = coding.getCode();
447                                } else {
448                                        throw new InvalidRequestException(Msg.code(843) + "A code must be provided for translation to occur.");
449                                }
450
451                                if (coding.hasSystem()) {
452                                        predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), coding.getSystem()));
453                                        targetCodeSystem = coding.getSystem();
454                                }
455
456                                if (coding.hasVersion()) {
457                                        predicates.add(criteriaBuilder.equal(groupJoin.get("myTargetVersion"), coding.getVersion()));
458                                }
459
460                                if (translationQuery.hasUrl()) {
461                                        predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myUrl"), translationQuery.getUrl().getValueAsString()));
462                                        if (translationQuery.hasConceptMapVersion()) {
463                                                // both url and conceptMapVersion
464                                                predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), translationQuery.getConceptMapVersion().getValueAsString()));
465                                        } else {
466                                                if (StringUtils.isNotBlank(latestConceptMapVersion)) {
467                                                        // only url and use latestConceptMapVersion
468                                                        predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myVersion"), latestConceptMapVersion));
469                                                } else {
470                                                        predicates.add(criteriaBuilder.isNull(conceptMapJoin.get("myVersion")));
471                                                }
472                                        }
473                                }
474
475                                if (translationQuery.hasTargetSystem()) {
476                                        predicates.add(criteriaBuilder.equal(groupJoin.get("mySource"), translationQuery.getTargetSystem().getValueAsString()));
477                                }
478
479                                if (translationQuery.hasSource()) {
480                                        predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myTarget"), translationQuery.getSource().getValueAsString()));
481                                }
482
483                                if (translationQuery.hasTarget()) {
484                                        predicates.add(criteriaBuilder.equal(conceptMapJoin.get("mySource"), translationQuery.getTarget().getValueAsString()));
485                                }
486
487                                if (translationQuery.hasResourceId()) {
488                                        predicates.add(criteriaBuilder.equal(conceptMapJoin.get("myResourcePid"), translationQuery.getResourceId()));
489                                }
490
491                                Predicate outerPredicate = criteriaBuilder.and(predicates.toArray(new Predicate[0]));
492                                query.where(outerPredicate);
493
494                                // Use scrollable results.
495                                final TypedQuery<TermConceptMapGroupElement> typedQuery = myEntityManager.createQuery(query.select(root));
496                                org.hibernate.query.Query<TermConceptMapGroupElement> hibernateQuery = (org.hibernate.query.Query<TermConceptMapGroupElement>) typedQuery;
497                                hibernateQuery.setFetchSize(myFetchSize);
498                                ScrollableResults scrollableResults = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
499                                try (ScrollableResultsIterator<TermConceptMapGroupElement> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults)) {
500
501                                        Set<TermConceptMapGroupElementTarget> matches = new HashSet<>();
502                                        while (scrollableResultsIterator.hasNext()) {
503                                                TermConceptMapGroupElement nextElement = scrollableResultsIterator.next();
504
505                                                /* TODO: The invocation of the size() below does not seem to be necessary but for some reason,
506                                                 * but removing it causes tests in TerminologySvcImplR4Test to fail. We use the outcome
507                                                 * in a trace log to avoid ErrorProne flagging an unused return value.
508                                                 */
509                                                int size = nextElement.getConceptMapGroupElementTargets().size();
510                                                ourLog.trace("Have {} targets", size);
511
512                                                myEntityManager.detach(nextElement);
513
514                                                if (isNotBlank(targetCode)) {
515                                                        for (TermConceptMapGroupElementTarget next : nextElement.getConceptMapGroupElementTargets()) {
516                                                                if (matches.add(next)) {
517                                                                        if (isBlank(targetCodeSystem) || StringUtils.equals(targetCodeSystem, next.getSystem())) {
518                                                                                if (StringUtils.equals(targetCode, next.getCode())) {
519                                                                                        TranslateConceptResult translationMatch = new TranslateConceptResult();
520                                                                                        translationMatch.setCode(nextElement.getCode());
521                                                                                        translationMatch.setSystem(nextElement.getSystem());
522                                                                                        translationMatch.setSystemVersion(nextElement.getSystemVersion());
523                                                                                        translationMatch.setDisplay(nextElement.getDisplay());
524                                                                                        translationMatch.setValueSet(nextElement.getValueSet());
525                                                                                        translationMatch.setSystemVersion(nextElement.getSystemVersion());
526                                                                                        translationMatch.setConceptMapUrl(nextElement.getConceptMapUrl());
527                                                                                        if (next.getEquivalence() != null) {
528                                                                                                translationMatch.setEquivalence(next.getEquivalence().toCode());
529                                                                                        }
530
531                                                                                        if (alreadyContainsMapping(elements, translationMatch) || alreadyContainsMapping(retVal.getResults(), translationMatch)) {
532                                                                                                continue;
533                                                                                        }
534
535                                                                                        elements.add(translationMatch);
536                                                                                }
537                                                                        }
538
539                                                                }
540                                                        }
541                                                }
542                                        }
543
544                                }
545
546                                ourLastResultsFromTranslationWithReverseCache = false; // For testing.
547                                myMemoryCacheService.put(MemoryCacheService.CacheEnum.CONCEPT_TRANSLATION_REVERSE, translationQuery, elements);
548                                retVal.getResults().addAll(elements);
549                        } else {
550                                ourLastResultsFromTranslationWithReverseCache = true; // For testing.
551                                retVal.getResults().addAll(cachedElements);
552                        }
553                }
554
555                buildTranslationResult(retVal);
556                return retVal;
557        }
558
559        private boolean alreadyContainsMapping(List<TranslateConceptResult> elements, TranslateConceptResult translationMatch) {
560                for (TranslateConceptResult nextExistingElement : elements) {
561                        if (StringUtils.equals(nextExistingElement.getSystem(), translationMatch.getSystem())) {
562                                if (StringUtils.equals(nextExistingElement.getSystemVersion(), translationMatch.getSystemVersion())) {
563                                        if (StringUtils.equals(nextExistingElement.getCode(), translationMatch.getCode())) {
564                                                return true;
565                                        }
566                                }
567                        }
568                }
569                return false;
570        }
571
572        public void deleteConceptMap(ResourceTable theResourceTable) {
573                // Get existing entity so it can be deleted.
574                Optional<TermConceptMap> optionalExistingTermConceptMapById = myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId());
575
576                if (optionalExistingTermConceptMapById.isPresent()) {
577                        TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get();
578
579                        ourLog.info("Deleting existing TermConceptMap[{}] and its children...", existingTermConceptMap.getId());
580                        for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) {
581
582                                for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) {
583
584                                        for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) {
585
586                                                myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId());
587                                        }
588
589                                        myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId());
590                                }
591
592                                myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId());
593                        }
594
595                        myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId());
596                        ourLog.info("Done deleting existing TermConceptMap[{}] and its children.", existingTermConceptMap.getId());
597                }
598        }
599
600        // Special case for the translate operation with url and without
601        // conceptMapVersion, find the latest conecptMapVersion
602        private String getLatestConceptMapVersion(TranslationRequest theTranslationRequest) {
603
604                Pageable page = PageRequest.of(0, 1);
605                List<TermConceptMap> theConceptMapList = myConceptMapDao.getTermConceptMapEntitiesByUrlOrderByMostRecentUpdate(page,
606                        theTranslationRequest.getUrl().asStringValue());
607                if (!theConceptMapList.isEmpty()) {
608                        return theConceptMapList.get(0).getVersion();
609                }
610
611                return null;
612        }
613
614        private void buildTranslationResult(TranslateConceptResults theTranslationResult) {
615
616                String msg;
617                if (theTranslationResult.getResults().isEmpty()) {
618                        theTranslationResult.setResult(false);
619                        msg = myContext.getLocalizer().getMessage(TermConceptMappingSvcImpl.class, "noMatchesFound");
620                        theTranslationResult.setMessage(msg);
621                } else {
622                        theTranslationResult.setResult(true);
623                        msg = myContext.getLocalizer().getMessage(TermConceptMappingSvcImpl.class, "matchesFound");
624                        theTranslationResult.setMessage(msg);
625                }
626
627        }
628
629
630        /**
631         * This method is present only for unit tests, do not call from client code
632         */
633        @VisibleForTesting
634        public static void clearOurLastResultsFromTranslationCache() {
635                ourLastResultsFromTranslationCache = false;
636        }
637
638        /**
639         * This method is present only for unit tests, do not call from client code
640         */
641        @VisibleForTesting
642        public static void clearOurLastResultsFromTranslationWithReverseCache() {
643                ourLastResultsFromTranslationWithReverseCache = false;
644        }
645
646        /**
647         * This method is present only for unit tests, do not call from client code
648         */
649        @VisibleForTesting
650        static boolean isOurLastResultsFromTranslationCache() {
651                return ourLastResultsFromTranslationCache;
652        }
653
654        /**
655         * This method is present only for unit tests, do not call from client code
656         */
657        @VisibleForTesting
658        static boolean isOurLastResultsFromTranslationWithReverseCache() {
659                return ourLastResultsFromTranslationWithReverseCache;
660        }
661
662        public static Parameters toParameters(TranslateConceptResults theTranslationResult) {
663                Parameters retVal = new Parameters();
664
665                retVal.addParameter().setName("result").setValue(new BooleanType(theTranslationResult.getResult()));
666
667                if (theTranslationResult.getMessage() != null) {
668                        retVal.addParameter().setName("message").setValue(new StringType(theTranslationResult.getMessage()));
669                }
670
671                for (TranslateConceptResult translationMatch : theTranslationResult.getResults()) {
672                        Parameters.ParametersParameterComponent matchParam = retVal.addParameter().setName("match");
673                        populateTranslateMatchParts(translationMatch, matchParam);
674                }
675
676                return retVal;
677        }
678
679        private static void populateTranslateMatchParts(TranslateConceptResult theTranslationMatch, Parameters.ParametersParameterComponent theParam) {
680                if (theTranslationMatch.getEquivalence() != null) {
681                        theParam.addPart().setName("equivalence").setValue(new CodeType(theTranslationMatch.getEquivalence()));
682                }
683
684                if (isNotBlank(theTranslationMatch.getSystem()) || isNotBlank(theTranslationMatch.getCode()) || isNotBlank(theTranslationMatch.getDisplay())) {
685                        Coding value = new Coding(theTranslationMatch.getSystem(), theTranslationMatch.getCode(), theTranslationMatch.getDisplay());
686
687                        if (isNotBlank(theTranslationMatch.getSystemVersion())) {
688                                value.setVersion(theTranslationMatch.getSystemVersion());
689                        }
690
691                        theParam.addPart().setName("concept").setValue(value);
692                }
693
694                if (isNotBlank(theTranslationMatch.getConceptMapUrl())) {
695                        theParam.addPart().setName("source").setValue(new UriType(theTranslationMatch.getConceptMapUrl()));
696                }
697        }
698}