001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2025 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.FhirVersionEnum;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.interceptor.model.RequestPartitionId;
026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
027import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
028import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
029import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
030import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao;
031import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao;
032import ca.uhn.fhir.jpa.dao.data.ITermConceptDao;
033import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao;
034import ca.uhn.fhir.jpa.dao.data.ITermConceptParentChildLinkDao;
035import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao;
036import ca.uhn.fhir.jpa.entity.TermCodeSystem;
037import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
038import ca.uhn.fhir.jpa.entity.TermConcept;
039import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
040import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
041import ca.uhn.fhir.jpa.entity.TermConceptProperty;
042import ca.uhn.fhir.jpa.model.dao.JpaPid;
043import ca.uhn.fhir.jpa.model.entity.EntityIndexStatusEnum;
044import ca.uhn.fhir.jpa.model.entity.ResourceTable;
045import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
046import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
047import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
048import ca.uhn.fhir.jpa.term.api.ITermVersionAdapterSvc;
049import ca.uhn.fhir.jpa.term.custom.CustomTerminologySet;
050import ca.uhn.fhir.rest.api.Constants;
051import ca.uhn.fhir.rest.api.server.RequestDetails;
052import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
053import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
054import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
055import ca.uhn.fhir.util.ObjectUtil;
056import ca.uhn.fhir.util.UrlUtil;
057import ca.uhn.fhir.util.ValidateUtil;
058import jakarta.annotation.Nonnull;
059import jakarta.persistence.EntityManager;
060import jakarta.persistence.PersistenceContext;
061import jakarta.persistence.PersistenceContextType;
062import org.apache.commons.lang3.Validate;
063import org.hl7.fhir.instance.model.api.IIdType;
064import org.hl7.fhir.r4.model.CodeSystem;
065import org.hl7.fhir.r4.model.ConceptMap;
066import org.hl7.fhir.r4.model.ValueSet;
067import org.slf4j.Logger;
068import org.slf4j.LoggerFactory;
069import org.springframework.beans.factory.annotation.Autowired;
070import org.springframework.transaction.annotation.Propagation;
071import org.springframework.transaction.annotation.Transactional;
072import org.springframework.transaction.support.TransactionSynchronizationManager;
073
074import java.util.ArrayList;
075import java.util.Arrays;
076import java.util.Collection;
077import java.util.Collections;
078import java.util.Date;
079import java.util.HashMap;
080import java.util.HashSet;
081import java.util.IdentityHashMap;
082import java.util.List;
083import java.util.Map;
084import java.util.Objects;
085import java.util.Optional;
086import java.util.Set;
087import java.util.UUID;
088import java.util.concurrent.atomic.AtomicInteger;
089import java.util.stream.Collectors;
090
091import static org.apache.commons.lang3.StringUtils.isBlank;
092import static org.apache.commons.lang3.StringUtils.isNotBlank;
093import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_LOW;
094
095public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
096        private static final Logger ourLog = LoggerFactory.getLogger(TermCodeSystemStorageSvcImpl.class);
097        private static final Object PLACEHOLDER_OBJECT = new Object();
098
099        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
100        protected EntityManager myEntityManager;
101
102        @Autowired
103        protected ITermCodeSystemDao myCodeSystemDao;
104
105        @Autowired
106        protected ITermCodeSystemVersionDao myCodeSystemVersionDao;
107
108        @Autowired
109        protected ITermConceptDao myConceptDao;
110
111        @Autowired
112        protected ITermConceptPropertyDao myConceptPropertyDao;
113
114        @Autowired
115        protected ITermConceptDesignationDao myConceptDesignationDao;
116
117        @Autowired
118        protected IIdHelperService<JpaPid> myIdHelperService;
119
120        @Autowired
121        private ITermConceptParentChildLinkDao myConceptParentChildLinkDao;
122
123        @Autowired
124        private ITermVersionAdapterSvc myTerminologyVersionAdapterSvc;
125
126        @Autowired
127        private ITermDeferredStorageSvc myDeferredStorageSvc;
128
129        @Autowired
130        private FhirContext myContext;
131
132        @Autowired
133        private ITermReadSvc myTerminologySvc;
134
135        @Autowired
136        private JpaStorageSettings myStorageSettings;
137
138        @Autowired
139        private IResourceTableDao myResourceTableDao;
140
141        @Autowired
142        private TermConceptDaoSvc myTermConceptDaoSvc;
143
144        @Transactional
145        @Override
146        public UploadStatistics applyDeltaCodeSystemsAdd(String theSystem, CustomTerminologySet theAdditions) {
147                ValidateUtil.isNotBlankOrThrowInvalidRequest(theSystem, "No system provided");
148                validateDstu3OrNewer();
149                theAdditions.validateNoCycleOrThrowInvalidRequest();
150
151                TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(theSystem);
152                if (cs == null) {
153                        CodeSystem codeSystemResource = new CodeSystem();
154                        codeSystemResource.setUrl(theSystem);
155                        codeSystemResource.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT);
156                        if (isBlank(codeSystemResource.getIdElement().getIdPart()) && theSystem.contains(LOINC_LOW)) {
157                                codeSystemResource.setId(LOINC_LOW);
158                        }
159                        myTerminologyVersionAdapterSvc.createOrUpdateCodeSystem(codeSystemResource);
160
161                        cs = myCodeSystemDao.findByCodeSystemUri(theSystem);
162                }
163
164                TermCodeSystemVersion csv = cs.getCurrentVersion();
165                Validate.notNull(csv);
166
167                CodeSystem codeSystem = myTerminologySvc.fetchCanonicalCodeSystemFromCompleteContext(theSystem);
168                if (codeSystem.getContent() != CodeSystem.CodeSystemContentMode.NOTPRESENT) {
169                        throw new InvalidRequestException(
170                                        Msg.code(844) + "CodeSystem with url[" + Constants.codeSystemWithDefaultDescription(theSystem)
171                                                        + "] can not apply a delta - wrong content mode: " + codeSystem.getContent());
172                }
173
174                Validate.notNull(cs);
175                Validate.notNull(cs.getPid());
176
177                IIdType codeSystemId = cs.getResource().getIdDt();
178
179                UploadStatistics retVal = new UploadStatistics(codeSystemId);
180                HashMap<String, TermConcept> codeToConcept = new HashMap<>();
181
182                // Add root concepts
183                for (TermConcept nextRootConcept : theAdditions.getRootConcepts()) {
184                        List<String> parentCodes = Collections.emptyList();
185                        addConceptInHierarchy(csv, parentCodes, nextRootConcept, retVal, codeToConcept, 0);
186                }
187
188                return retVal;
189        }
190
191        @Transactional
192        @Override
193        public UploadStatistics applyDeltaCodeSystemsRemove(String theSystem, CustomTerminologySet theValue) {
194                ValidateUtil.isNotBlankOrThrowInvalidRequest(theSystem, "No system provided");
195                validateDstu3OrNewer();
196
197                TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(theSystem);
198                if (cs == null) {
199                        throw new InvalidRequestException(Msg.code(845) + "Unknown code system: " + theSystem);
200                }
201                IIdType target = cs.getResource().getIdDt();
202
203                AtomicInteger removeCounter = new AtomicInteger(0);
204
205                // We need to delete all termconcepts, and their children. This stream flattens the TermConcepts and their
206                // children into a single set of TermConcept objects retrieved from the DB. Note that we have to do this because
207                // deleteById() in JPA doesnt appear to actually commit or flush a transaction until way later, and we end up
208                // iterating multiple times over the same elements, which screws up our counter.
209
210                // Grab the actual entities
211                List<TermConcept> collect = theValue.getRootConcepts().stream()
212                                .map(val -> myTerminologySvc.findCode(theSystem, val.getCode()))
213                                .filter(Optional::isPresent)
214                                .map(Optional::get)
215                                .collect(Collectors.toList());
216
217                // Iterate over the actual entities and fill out their children
218                Set<TermConcept> allFoundTermConcepts = collect.stream()
219                                .flatMap(concept -> flattenChildren(concept).stream())
220                                .map(suppliedTermConcept -> myTerminologySvc.findCode(theSystem, suppliedTermConcept.getCode()))
221                                .filter(Optional::isPresent)
222                                .map(Optional::get)
223                                .collect(Collectors.toSet());
224
225                // Delete everything about these codes.
226                for (TermConcept code : allFoundTermConcepts) {
227                        deleteEverythingRelatedToConcept(code, removeCounter);
228                }
229
230                return new UploadStatistics(removeCounter.get(), target);
231        }
232
233        private void deleteEverythingRelatedToConcept(TermConcept theConcept, AtomicInteger theRemoveCounter) {
234
235                for (TermConceptParentChildLink nextParent : theConcept.getParents()) {
236                        nextParent.getParent().getChildren().remove(nextParent);
237                        myConceptParentChildLinkDao.deleteById(nextParent.getPid());
238                }
239                for (TermConceptParentChildLink nextChild : theConcept.getChildren()) {
240                        nextChild.getChild().getParents().remove(nextChild);
241                        myConceptParentChildLinkDao.deleteById(nextChild.getPid());
242                }
243
244                for (TermConceptDesignation next : theConcept.getDesignations()) {
245                        myConceptDesignationDao.deleteById(next.getPartitionedId());
246                }
247                theConcept.getDesignations().clear();
248                for (TermConceptProperty next : theConcept.getProperties()) {
249                        myConceptPropertyDao.deleteById(next.getPartitionedId());
250                }
251                theConcept.getProperties().clear();
252
253                ourLog.info("Deleting concept {} - Code {}", theConcept.getId(), theConcept.getCode());
254
255                myConceptDao.deleteById(theConcept.getPid());
256                //              myEntityManager.remove(theConcept);
257
258                theRemoveCounter.incrementAndGet();
259        }
260
261        private List<TermConcept> flattenChildren(TermConcept theTermConcept) {
262                if (theTermConcept.getChildren().isEmpty()) {
263                        return Arrays.asList(theTermConcept);
264                }
265
266                // Recursively flatten children
267                List<TermConcept> childTermConcepts = theTermConcept.getChildren().stream()
268                                .map(TermConceptParentChildLink::getChild)
269                                .flatMap(childConcept -> flattenChildren(childConcept).stream())
270                                .collect(Collectors.toList());
271
272                // Add itself before its list of children
273                childTermConcepts.add(0, theTermConcept);
274                return childTermConcepts;
275        }
276
277        /**
278         * Returns the number of saved concepts
279         */
280        @Override
281        public int saveConcept(TermConcept theConcept) {
282                return myTermConceptDaoSvc.saveConcept(theConcept);
283        }
284
285        @Override
286        @Transactional(propagation = Propagation.MANDATORY)
287        public void storeNewCodeSystemVersionIfNeeded(
288                        CodeSystem theCodeSystem, ResourceTable theResourceEntity, RequestDetails theRequestDetails) {
289                if (theCodeSystem != null && isNotBlank(theCodeSystem.getUrl())) {
290                        String codeSystemUrl = theCodeSystem.getUrl();
291                        if (theCodeSystem.getContent() == CodeSystem.CodeSystemContentMode.COMPLETE
292                                        || theCodeSystem.getContent() == null
293                                        || theCodeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT) {
294                                ourLog.info(
295                                                "CodeSystem {} has a status of {}, going to store concepts in terminology tables",
296                                                theResourceEntity.getIdDt().getValue(),
297                                                theCodeSystem.getContentElement().getValueAsString());
298
299                                detectDuplicatesInCodeSystem(theCodeSystem);
300
301                                /*
302                                 * If this is a not-present codesystem and codesystem version already exists, we don't want to
303                                 * overwrite the existing version since that will wipe out the existing concepts. We do create
304                                 * or update the TermCodeSystem table though, since that allows the DB to reject changes that would
305                                 * result in duplicate CodeSystem.url values.
306                                 */
307                                if (theCodeSystem.getContent() == CodeSystem.CodeSystemContentMode.NOTPRESENT) {
308                                        TermCodeSystem termCodeSystem = myCodeSystemDao.findByCodeSystemUri(theCodeSystem.getUrl());
309                                        if (termCodeSystem != null) {
310                                                TermCodeSystemVersion codeSystemVersion =
311                                                                getExistingTermCodeSystemVersion(termCodeSystem.getPid(), theCodeSystem.getVersion());
312                                                if (codeSystemVersion != null) {
313                                                        getOrCreateDistinctTermCodeSystem(
314                                                                        theCodeSystem.getUrl(),
315                                                                        theCodeSystem.getUrl(),
316                                                                        theCodeSystem.getVersion(),
317                                                                        theResourceEntity);
318                                                        return;
319                                                }
320                                        }
321                                }
322
323                                TermCodeSystemVersion persCs = new TermCodeSystemVersion();
324                                populateCodeSystemVersionProperties(persCs, theCodeSystem, theResourceEntity);
325                                myEntityManager.persist(persCs);
326
327                                persCs.getConcepts().addAll(TermReadSvcImpl.toPersistedConcepts(theCodeSystem.getConcept(), persCs));
328                                ourLog.debug("Code system has {} concepts", persCs.getConcepts().size());
329                                storeNewCodeSystemVersion(
330                                                codeSystemUrl,
331                                                theCodeSystem.getName(),
332                                                theCodeSystem.getVersion(),
333                                                persCs,
334                                                theResourceEntity,
335                                                theRequestDetails);
336                        }
337                }
338        }
339
340        private static void detectDuplicatesInCodeSystem(CodeSystem theCodeSystem) {
341                detectDuplicatesInCodeSystem(theCodeSystem.getConcept(), new HashSet<>());
342        }
343
344        private static void detectDuplicatesInCodeSystem(
345                        List<CodeSystem.ConceptDefinitionComponent> theCodeList, Set<String> theFoundCodesBuffer) {
346                for (var next : theCodeList) {
347                        if (isNotBlank(next.getCode())) {
348                                if (!theFoundCodesBuffer.add(next.getCode())) {
349                                        /*
350                                         * Note: We could possibly modify this behaviour to be forgiving, and just
351                                         * ignore duplicates. The only issue is that concepts can have properties,
352                                         * designations, etc. and it could be dangerous to just pick one and ignore the
353                                         * other. So the safer thing seems to be to just throw an error.
354                                         */
355                                        throw new PreconditionFailedException(Msg.code(2528) + "Duplicate concept detected in CodeSystem: "
356                                                        + UrlUtil.sanitizeUrlPart(next.getCode()));
357                                }
358                        }
359                        // Test child concepts within the parent concept
360                        detectDuplicatesInCodeSystem(next.getConcept(), theFoundCodesBuffer);
361                }
362        }
363
364        @Override
365        @Transactional
366        public IIdType storeNewCodeSystemVersion(
367                        CodeSystem theCodeSystemResource,
368                        TermCodeSystemVersion theCodeSystemVersion,
369                        RequestDetails theRequest,
370                        List<ValueSet> theValueSets,
371                        List<ConceptMap> theConceptMaps) {
372                assert TransactionSynchronizationManager.isActualTransactionActive();
373
374                Validate.notBlank(theCodeSystemResource.getUrl(), "theCodeSystemResource must have a URL");
375
376                // Note that this creates the TermCodeSystem and TermCodeSystemVersion entities if needed
377                IIdType csId = myTerminologyVersionAdapterSvc.createOrUpdateCodeSystem(theCodeSystemResource, theRequest);
378
379                JpaPid codeSystemResourcePid = myIdHelperService.resolveResourceIdentityPid(
380                                RequestPartitionId.allPartitions(),
381                                csId.getResourceType(),
382                                csId.getIdPart(),
383                                ResolveIdentityMode.includeDeleted().cacheOk());
384                ResourceTable resource = myResourceTableDao.getOne(codeSystemResourcePid);
385
386                ourLog.info("CodeSystem resource has ID: {}", csId.getValue());
387
388                populateCodeSystemVersionProperties(theCodeSystemVersion, theCodeSystemResource, resource);
389
390                storeNewCodeSystemVersion(
391                                theCodeSystemResource.getUrl(),
392                                theCodeSystemResource.getName(),
393                                theCodeSystemResource.getVersion(),
394                                theCodeSystemVersion,
395                                resource,
396                                theRequest);
397
398                myDeferredStorageSvc.addConceptMapsToStorageQueue(theConceptMaps);
399                myDeferredStorageSvc.addValueSetsToStorageQueue(theValueSets);
400
401                return csId;
402        }
403
404        @Override
405        @Transactional
406        public void storeNewCodeSystemVersion(
407                        String theSystemUri,
408                        String theSystemName,
409                        String theCodeSystemVersionId,
410                        TermCodeSystemVersion theCodeSystemVersion,
411                        ResourceTable theCodeSystemResourceTable,
412                        RequestDetails theRequestDetails) {
413                assert TransactionSynchronizationManager.isActualTransactionActive();
414
415                ourLog.debug("Storing code system");
416                Date updated = new Date();
417
418                TermCodeSystemVersion codeSystemToStore = theCodeSystemVersion;
419                ValidateUtil.isTrueOrThrowInvalidRequest(codeSystemToStore.getResource() != null, "No resource supplied");
420                ValidateUtil.isNotBlankOrThrowInvalidRequest(theSystemUri, "No system URI supplied");
421
422                TermCodeSystem codeSystem = getOrCreateDistinctTermCodeSystem(
423                                theSystemUri, theSystemName, theCodeSystemVersionId, theCodeSystemResourceTable);
424
425                List<TermCodeSystemVersion> existing =
426                                myCodeSystemVersionDao.findByCodeSystemResourcePid(theCodeSystemResourceTable.getResourceId());
427                for (TermCodeSystemVersion next : existing) {
428                        if (Objects.equals(next.getCodeSystemVersionId(), theCodeSystemVersionId)
429                                        && myConceptDao.countByCodeSystemVersion(next.getPid()) == 0) {
430
431                                /*
432                                 * If we already have a CodeSystemVersion that matches the version we're storing, we
433                                 * can reuse it. Note that we only reuse if there are no concepts attached to the
434                                 * existing codesystem because we always write a completely fresh set of concepts
435                                 * and mark the old one for deletion. Theoretically we could optimize this by
436                                 * figuring out a delta and only writing that, but that is fairly involved
437                                 * since concepts have parents and children and properties and designations and
438                                 * all that - so it's safer to just always assume changes and write everything
439                                 * fresh. Also, this isn't the kind of thing that's expected to happen often
440                                 * so we aren't particularly performance sensitive here.
441                                 */
442                                next.setCodeSystemDisplayName(theSystemName);
443                                codeSystemToStore = next;
444
445                        } else {
446
447                                /*
448                                 * If we already have a TermCodeSystemVersion that corresponds to the FHIR Resource ID we're
449                                 * adding a version to, we will mark it for deletion. For any one resource there can only
450                                 * be one TermCodeSystemVersion entity in the DB. Multiple versions of a codesystem uses
451                                 * multiple CodeSystem resources with CodeSystem.version set differently (as opposed to
452                                 * multiple versions of the same CodeSystem, where CodeSystem.meta.versionId is different)
453                                 */
454                                next.setCodeSystemVersionId("DELETED_" + UUID.randomUUID());
455                                myCodeSystemVersionDao.saveAndFlush(next);
456                                myDeferredStorageSvc.deleteCodeSystemVersion(next);
457                        }
458                }
459
460                /*
461                 * Do the upload
462                 */
463
464                codeSystemToStore.setCodeSystem(codeSystem);
465                codeSystemToStore.setCodeSystemDisplayName(theSystemName);
466                codeSystemToStore.setCodeSystemVersionId(theCodeSystemVersionId);
467
468                if (codeSystemToStore.getPid() == null) {
469                        myEntityManager.persist(codeSystemToStore);
470                }
471
472                ourLog.debug("Validating all codes in CodeSystem for storage (this can take some time for large sets)");
473
474                // Validate the code system
475                ArrayList<String> conceptsStack = new ArrayList<>();
476                IdentityHashMap<TermConcept, Object> allConcepts = new IdentityHashMap<>();
477                int totalCodeCount = 0;
478                Collection<TermConcept> conceptsToSave = theCodeSystemVersion.getConcepts();
479                for (TermConcept next : conceptsToSave) {
480                        totalCodeCount += validateConceptForStorage(next, codeSystemToStore, conceptsStack, allConcepts);
481                        Validate.isTrue(next.getPid().getId() == null);
482                        Validate.isTrue(codeSystemToStore.getPid() != null);
483
484                        // Make sure to initialize the PK object so that hibernate doesn't choke on creation
485                        next.setId(null);
486
487                        next.setCodeSystemVersion(codeSystemToStore);
488                        next.setUpdated(updated);
489
490                        myEntityManager.persist(next);
491                        for (var property : next.getProperties()) {
492                                assert property.getId() == null;
493                                property.setCodeSystemVersion(codeSystemToStore);
494                                myEntityManager.persist(property);
495                        }
496                        for (var designation : next.getDesignations()) {
497                                assert designation.getId() == null;
498                                designation.setCodeSystemVersion(codeSystemToStore);
499                                myEntityManager.persist(designation);
500                        }
501                }
502
503                ourLog.debug("Saving version containing {} concepts", totalCodeCount);
504                Validate.notNull(codeSystemToStore.getPid(), "Code system not saved");
505                codeSystemToStore = myEntityManager.merge(codeSystemToStore);
506
507                boolean isMakeVersionCurrent = ITermCodeSystemStorageSvc.isMakeVersionCurrent(theRequestDetails);
508                if (isMakeVersionCurrent) {
509                        codeSystem.setCurrentVersion(codeSystemToStore);
510                        if (codeSystem.getPid() == null) {
511                                codeSystem = myCodeSystemDao.saveAndFlush(codeSystem);
512                        }
513                }
514
515                ourLog.debug("Setting CodeSystemVersion[{}] on {} concepts...", codeSystem.getPid(), totalCodeCount);
516                for (TermConcept next : conceptsToSave) {
517                        populateVersion(next, codeSystemToStore);
518                }
519
520                ourLog.debug("Saving {} concepts...", totalCodeCount);
521                IdentityHashMap<TermConcept, Object> conceptsStack2 = new IdentityHashMap<>();
522                for (TermConcept next : conceptsToSave) {
523                        persistChildren(next, codeSystemToStore, conceptsStack2, totalCodeCount);
524                }
525
526                ourLog.debug("Done saving concepts, flushing to database");
527                if (!myDeferredStorageSvc.isStorageQueueEmpty(true)) {
528                        ourLog.info("Note that some concept saving has been deferred");
529                }
530        }
531
532        private TermCodeSystemVersion getExistingTermCodeSystemVersion(
533                        Long theCodeSystemVersionPid, String theCodeSystemVersion) {
534                TermCodeSystemVersion existing;
535                if (theCodeSystemVersion == null) {
536                        existing = myCodeSystemVersionDao.findByCodeSystemPidVersionIsNull(theCodeSystemVersionPid);
537                } else {
538                        existing =
539                                        myCodeSystemVersionDao.findByCodeSystemPidAndVersion(theCodeSystemVersionPid, theCodeSystemVersion);
540                }
541
542                return existing;
543        }
544
545        private void validateDstu3OrNewer() {
546                Validate.isTrue(
547                                myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3),
548                                "Terminology operations only supported in DSTU3+ mode");
549        }
550
551        private void addConceptInHierarchy(
552                        TermCodeSystemVersion theCsv,
553                        Collection<String> theParentCodes,
554                        TermConcept theConceptToAdd,
555                        UploadStatistics theStatisticsTracker,
556                        Map<String, TermConcept> theCodeToConcept,
557                        int theSequence) {
558                TermConcept conceptToAdd = theConceptToAdd;
559                List<TermConceptParentChildLink> childrenToAdd = theConceptToAdd.getChildren();
560
561                String nextCodeToAdd = conceptToAdd.getCode();
562                String parentDescription = "(root concept)";
563
564                ourLog.info(
565                                "Saving concept {} with parent {}", theStatisticsTracker.getUpdatedConceptCount(), parentDescription);
566
567                Optional<TermConcept> existingCodeOpt = myConceptDao.findByCodeSystemAndCode(theCsv.getPid(), nextCodeToAdd);
568                List<TermConceptParentChildLink> existingParentLinks;
569                if (existingCodeOpt.isPresent()) {
570                        TermConcept existingCode = existingCodeOpt.get();
571                        existingCode.setIndexStatus(null);
572                        existingCode.setDisplay(conceptToAdd.getDisplay());
573                        conceptToAdd = existingCode;
574                        existingParentLinks = conceptToAdd.getParents();
575                } else {
576                        existingParentLinks = Collections.emptyList();
577                }
578
579                Set<TermConcept> parentConceptsWeShouldLinkTo = new HashSet<>();
580                for (String nextParentCode : theParentCodes) {
581
582                        // Don't add parent links that already exist for the code
583                        if (existingParentLinks.stream()
584                                        .anyMatch(t -> t.getParent().getCode().equals(nextParentCode))) {
585                                continue;
586                        }
587
588                        TermConcept nextParentOpt = theCodeToConcept.get(nextParentCode);
589                        if (nextParentOpt == null) {
590                                nextParentOpt = myConceptDao
591                                                .findByCodeSystemAndCode(theCsv.getPid(), nextParentCode)
592                                                .orElse(null);
593                        }
594                        if (nextParentOpt == null) {
595                                throw new InvalidRequestException(Msg.code(846) + "Unable to add code \"" + nextCodeToAdd
596                                                + "\" to unknown parent: " + nextParentCode);
597                        }
598                        parentConceptsWeShouldLinkTo.add(nextParentOpt);
599                }
600
601                if (conceptToAdd.getSequence() == null) {
602                        conceptToAdd.setSequence(theSequence);
603                }
604
605                // Null out the hierarchy PIDs for this concept always. We do this because we're going to
606                // force a reindex, and it'll be regenerated then
607                conceptToAdd.setParentPids(null);
608                conceptToAdd.setCodeSystemVersion(theCsv);
609
610                if (conceptToAdd.getProperties() != null)
611                        conceptToAdd.getProperties().forEach(termConceptProperty -> {
612                                termConceptProperty.setConcept(theConceptToAdd);
613                                termConceptProperty.setCodeSystemVersion(theCsv);
614                        });
615                if (theStatisticsTracker.getUpdatedConceptCount() <= myStorageSettings.getDeferIndexingForCodesystemsOfSize()) {
616                        saveConcept(conceptToAdd);
617                        Long nextConceptPid = conceptToAdd.getId();
618                        Objects.requireNonNull(nextConceptPid);
619                } else {
620                        myDeferredStorageSvc.addConceptToStorageQueue(conceptToAdd);
621                }
622
623                theCodeToConcept.put(conceptToAdd.getCode(), conceptToAdd);
624
625                theStatisticsTracker.incrementUpdatedConceptCount();
626
627                // Add link to new child to the parent
628                for (TermConcept nextParentConcept : parentConceptsWeShouldLinkTo) {
629                        TermConceptParentChildLink parentLink = new TermConceptParentChildLink();
630                        parentLink.setParent(nextParentConcept);
631                        parentLink.setChild(conceptToAdd);
632                        parentLink.setCodeSystem(theCsv);
633                        parentLink.setRelationshipType(TermConceptParentChildLink.RelationshipTypeEnum.ISA);
634                        nextParentConcept.getChildren().add(parentLink);
635                        conceptToAdd.getParents().add(parentLink);
636                        ourLog.info(
637                                        "Saving parent/child link - Parent[{}] Child[{}]",
638                                        parentLink.getParent().getCode(),
639                                        parentLink.getChild().getCode());
640
641                        if (theStatisticsTracker.getUpdatedConceptCount()
642                                        <= myStorageSettings.getDeferIndexingForCodesystemsOfSize()) {
643                                myConceptParentChildLinkDao.save(parentLink);
644                        } else {
645                                myDeferredStorageSvc.addConceptLinkToStorageQueue(parentLink);
646                        }
647                }
648
649                ourLog.trace("About to save parent-child links");
650
651                // Save children recursively
652                int childIndex = 0;
653                for (TermConceptParentChildLink nextChildConceptLink : new ArrayList<>(childrenToAdd)) {
654
655                        TermConcept nextChild = nextChildConceptLink.getChild();
656
657                        for (int i = 0; i < nextChild.getParents().size(); i++) {
658                                if (nextChild.getParents().get(i).getId() == null) {
659                                        String parentCode =
660                                                        nextChild.getParents().get(i).getParent().getCode();
661                                        TermConcept parentConcept = theCodeToConcept.get(parentCode);
662                                        if (parentConcept == null) {
663                                                parentConcept = myConceptDao
664                                                                .findByCodeSystemAndCode(theCsv.getPid(), parentCode)
665                                                                .orElse(null);
666                                        }
667                                        if (parentConcept == null) {
668                                                throw new IllegalArgumentException(Msg.code(847) + "Unknown parent code: " + parentCode);
669                                        }
670
671                                        nextChild.getParents().get(i).setParent(parentConcept);
672                                }
673                        }
674
675                        Collection<String> parentCodes = nextChild.getParents().stream()
676                                        .map(t -> t.getParent().getCode())
677                                        .collect(Collectors.toList());
678                        addConceptInHierarchy(theCsv, parentCodes, nextChild, theStatisticsTracker, theCodeToConcept, childIndex);
679
680                        childIndex++;
681                }
682        }
683
684        private void persistChildren(
685                        TermConcept theConcept,
686                        TermCodeSystemVersion theCodeSystem,
687                        IdentityHashMap<TermConcept, Object> theConceptsStack,
688                        int theTotalConcepts) {
689                if (theConceptsStack.put(theConcept, PLACEHOLDER_OBJECT) != null) {
690                        return;
691                }
692
693                if ((theConceptsStack.size() + 1) % 10000 == 0) {
694                        float pct = (float) theConceptsStack.size() / (float) theTotalConcepts;
695                        ourLog.info(
696                                        "Have processed {}/{} concepts ({}%)",
697                                        theConceptsStack.size(), theTotalConcepts, (int) (pct * 100.0f));
698                }
699
700                theConcept.setCodeSystemVersion(theCodeSystem);
701                theConcept.setIndexStatus(EntityIndexStatusEnum.INDEXED_ALL);
702
703                if (theConceptsStack.size() <= myStorageSettings.getDeferIndexingForCodesystemsOfSize()) {
704                        saveConcept(theConcept);
705                } else {
706                        myDeferredStorageSvc.addConceptToStorageQueue(theConcept);
707                }
708
709                for (TermConceptParentChildLink next : theConcept.getChildren()) {
710                        persistChildren(next.getChild(), theCodeSystem, theConceptsStack, theTotalConcepts);
711                }
712
713                for (TermConceptParentChildLink next : theConcept.getChildren()) {
714                        if (theConceptsStack.size() <= myStorageSettings.getDeferIndexingForCodesystemsOfSize()) {
715                                saveConceptLink(next);
716                        } else {
717                                myDeferredStorageSvc.addConceptLinkToStorageQueue(next);
718                        }
719                }
720        }
721
722        private void populateVersion(TermConcept theNext, TermCodeSystemVersion theCodeSystemVersion) {
723                theNext.setCodeSystemVersion(theCodeSystemVersion);
724                for (TermConceptParentChildLink next : theNext.getChildren()) {
725                        populateVersion(next.getChild(), theCodeSystemVersion);
726                }
727                theNext.getProperties().forEach(t -> t.setCodeSystemVersion(theCodeSystemVersion));
728                theNext.getDesignations().forEach(t -> t.setCodeSystemVersion(theCodeSystemVersion));
729        }
730
731        private void saveConceptLink(TermConceptParentChildLink next) {
732                if (next.getId() == null) {
733                        myConceptParentChildLinkDao.save(next);
734                }
735        }
736
737        @Nonnull
738        private TermCodeSystem getOrCreateDistinctTermCodeSystem(
739                        String theSystemUri,
740                        String theSystemName,
741                        String theSystemVersionId,
742                        ResourceTable theCodeSystemResourceTable) {
743                TermCodeSystem codeSystem = myCodeSystemDao.findByCodeSystemUri(theSystemUri);
744                if (codeSystem == null) {
745                        codeSystem = myCodeSystemDao.findByResourcePid((theCodeSystemResourceTable.getId()));
746                        if (codeSystem == null) {
747                                codeSystem = new TermCodeSystem();
748                        }
749                } else {
750                        checkForCodeSystemVersionDuplicate(
751                                        codeSystem, theSystemUri, theSystemVersionId, theCodeSystemResourceTable);
752                }
753
754                codeSystem.setResource(theCodeSystemResourceTable);
755                codeSystem.setCodeSystemUri(theSystemUri);
756                codeSystem.setName(theSystemName);
757                codeSystem = myCodeSystemDao.save(codeSystem);
758                return codeSystem;
759        }
760
761        private void checkForCodeSystemVersionDuplicate(
762                        TermCodeSystem theCodeSystem,
763                        String theSystemUri,
764                        String theSystemVersionId,
765                        ResourceTable theCodeSystemResourceTable) {
766                TermCodeSystemVersion codeSystemVersionEntity;
767                String msg = null;
768                if (theSystemVersionId == null) {
769                        // Check if a non-versioned TermCodeSystemVersion entity already exists for this TermCodeSystem.
770                        codeSystemVersionEntity = myCodeSystemVersionDao.findByCodeSystemPidVersionIsNull(theCodeSystem.getPid());
771                        if (codeSystemVersionEntity != null) {
772                                msg = myContext
773                                                .getLocalizer()
774                                                .getMessage(
775                                                                TermReadSvcImpl.class,
776                                                                "cannotCreateDuplicateCodeSystemUrl",
777                                                                theSystemUri,
778                                                                codeSystemVersionEntity
779                                                                                .getResource()
780                                                                                .getIdDt()
781                                                                                .toUnqualifiedVersionless()
782                                                                                .getValue());
783                        }
784                } else {
785                        // Check if a TermCodeSystemVersion entity already exists for this TermCodeSystem and version.
786                        codeSystemVersionEntity =
787                                        myCodeSystemVersionDao.findByCodeSystemPidAndVersion(theCodeSystem.getPid(), theSystemVersionId);
788                        if (codeSystemVersionEntity != null) {
789                                msg = myContext
790                                                .getLocalizer()
791                                                .getMessage(
792                                                                TermReadSvcImpl.class,
793                                                                "cannotCreateDuplicateCodeSystemUrlAndVersion",
794                                                                theSystemUri,
795                                                                theSystemVersionId,
796                                                                codeSystemVersionEntity
797                                                                                .getResource()
798                                                                                .getIdDt()
799                                                                                .toUnqualifiedVersionless()
800                                                                                .getValue());
801                        }
802                }
803                // Throw exception if the TermCodeSystemVersion is being duplicated.
804                if (codeSystemVersionEntity != null) {
805                        if (!ObjectUtil.equals(codeSystemVersionEntity.getResource().getId(), theCodeSystemResourceTable.getId())) {
806                                throw new UnprocessableEntityException(Msg.code(848) + msg);
807                        }
808                }
809        }
810
811        private void populateCodeSystemVersionProperties(
812                        TermCodeSystemVersion theCodeSystemVersion,
813                        CodeSystem theCodeSystemResource,
814                        ResourceTable theResourceTable) {
815                theCodeSystemVersion.setResource(theResourceTable);
816                theCodeSystemVersion.setCodeSystemDisplayName(theCodeSystemResource.getName());
817                theCodeSystemVersion.setCodeSystemVersionId(theCodeSystemResource.getVersion());
818        }
819
820        private int validateConceptForStorage(
821                        TermConcept theConcept,
822                        TermCodeSystemVersion theCodeSystemVersion,
823                        ArrayList<String> theConceptsStack,
824                        IdentityHashMap<TermConcept, Object> theAllConcepts) {
825                ValidateUtil.isTrueOrThrowInvalidRequest(
826                                theConcept.getCodeSystemVersion() != null, "CodeSystemVersion is null");
827                ValidateUtil.isNotBlankOrThrowInvalidRequest(
828                                theConcept.getCode(), "CodeSystem contains a code with no code value");
829
830                theConcept.setCodeSystemVersion(theCodeSystemVersion);
831                if (theConceptsStack.contains(theConcept.getCode())) {
832                        throw new InvalidRequestException(
833                                        Msg.code(849) + "CodeSystem contains circular reference around code " + theConcept.getCode());
834                }
835                theConceptsStack.add(theConcept.getCode());
836
837                int retVal = 0;
838                if (theAllConcepts.put(theConcept, theAllConcepts) == null) {
839                        if (theAllConcepts.size() % 1000 == 0) {
840                                ourLog.info("Have validated {} concepts", theAllConcepts.size());
841                        }
842                        retVal = 1;
843                }
844
845                for (TermConceptParentChildLink next : theConcept.getChildren()) {
846                        next.setCodeSystem(theCodeSystemVersion);
847                        retVal +=
848                                        validateConceptForStorage(next.getChild(), theCodeSystemVersion, theConceptsStack, theAllConcepts);
849                }
850
851                theConceptsStack.remove(theConceptsStack.size() - 1);
852
853                return retVal;
854        }
855}