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