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.cache;
021
022import ca.uhn.fhir.interceptor.model.RequestPartitionId;
023import ca.uhn.fhir.jpa.dao.data.IResourceIdentifierPatientUniqueEntityDao;
024import ca.uhn.fhir.jpa.dao.data.IResourceIdentifierSystemEntityDao;
025import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
026import ca.uhn.fhir.jpa.model.entity.ResourceIdentifierPatientUniqueEntity;
027import ca.uhn.fhir.jpa.model.entity.ResourceIdentifierSystemEntity;
028import ca.uhn.fhir.jpa.util.MemoryCacheService;
029import ca.uhn.fhir.rest.api.server.RequestDetails;
030import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
031import ca.uhn.fhir.util.SleepUtil;
032import jakarta.annotation.Nonnull;
033import jakarta.annotation.Nullable;
034import jakarta.persistence.EntityManager;
035import org.apache.commons.lang3.Validate;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038import org.springframework.transaction.annotation.Propagation;
039
040import java.util.function.Supplier;
041
042public class ResourceIdentifierCacheSvcImpl implements IResourceIdentifierCacheSvc {
043
044        private static final Logger ourLog = LoggerFactory.getLogger(ResourceIdentifierCacheSvcImpl.class);
045        private final MemoryCacheService myMemoryCache;
046        private final IResourceIdentifierSystemEntityDao myResourceIdentifierSystemEntityDao;
047        private final IResourceIdentifierPatientUniqueEntityDao myResourceIdentifierPatientUniqueEntityDao;
048        private final IHapiTransactionService myTransactionService;
049        private final EntityManager myEntityManager;
050
051        /**
052         * Constructor
053         */
054        public ResourceIdentifierCacheSvcImpl(
055                        IHapiTransactionService theTransactionService,
056                        MemoryCacheService theMemoryCache,
057                        IResourceIdentifierSystemEntityDao theResourceIdentifierSystemEntityDao,
058                        IResourceIdentifierPatientUniqueEntityDao theResourceIdentifierPatientUniqueEntityDao,
059                        EntityManager theEntityManager) {
060                myTransactionService = theTransactionService;
061                myMemoryCache = theMemoryCache;
062                myResourceIdentifierSystemEntityDao = theResourceIdentifierSystemEntityDao;
063                myResourceIdentifierPatientUniqueEntityDao = theResourceIdentifierPatientUniqueEntityDao;
064                myEntityManager = theEntityManager;
065        }
066
067        @Override
068        public long getOrCreateResourceIdentifierSystem(
069                        RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId, String theSystem) {
070                Validate.notBlank(theSystem, "theIdentifierSystem must not be blank");
071
072                Long pid =
073                                lookupResourceIdentifierSystemFromCacheOrDatabase(theRequestDetails, theRequestPartitionId, theSystem);
074
075                /*
076                 * If we don't find an existing entry for the system in the database,
077                 * we need to try to create a new one. We expect that identifier systems
078                 * are relatively unique
079                 */
080                if (pid == null) {
081                        int max = 5;
082                        for (int i = 0; i < max && pid == null; i++) {
083                                try {
084                                        pid = myTransactionService
085                                                        .withRequest(theRequestDetails)
086                                                        .withRequestPartitionId(theRequestPartitionId)
087                                                        .withPropagation(Propagation.REQUIRES_NEW)
088                                                        .execute(() -> {
089                                                                Long newPid = lookupResourceIdentifierSystemFromCacheOrDatabase(
090                                                                                theRequestDetails, theRequestPartitionId, theSystem);
091                                                                if (newPid == null) {
092                                                                        ResourceIdentifierSystemEntity newEntity = new ResourceIdentifierSystemEntity();
093                                                                        newEntity.setSystem(theSystem);
094                                                                        newEntity = myResourceIdentifierSystemEntityDao.save(newEntity);
095                                                                        assert newEntity.getPid() != null;
096                                                                        myMemoryCache.putAfterCommit(
097                                                                                        MemoryCacheService.CacheEnum.RESOURCE_IDENTIFIER_SYSTEM_TO_PID,
098                                                                                        theSystem,
099                                                                                        newEntity.getPid());
100                                                                        ourLog.info(
101                                                                                        "Created identifier System[{}] with PID: {}",
102                                                                                        theSystem,
103                                                                                        newEntity.getPid());
104                                                                        newPid = newEntity.getPid();
105                                                                }
106                                                                return newPid;
107                                                        });
108                                } catch (ResourceVersionConflictException e) {
109                                        ourLog.info(
110                                                        "Concurrency failure (attempt {}/{}) creating identifier system cache: {}",
111                                                        (i + 1),
112                                                        max,
113                                                        theSystem);
114                                        new SleepUtil().sleepAtLeast(500L);
115                                }
116                        }
117                }
118
119                Validate.isTrue(pid != null, "Failed to create resource identifier cache entry for system: %s", theSystem);
120                return pid;
121        }
122
123        private Long lookupResourceIdentifierSystemFromCacheOrDatabase(
124                        RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId, String theSystem) {
125                Long pid = myMemoryCache.get(
126                                MemoryCacheService.CacheEnum.RESOURCE_IDENTIFIER_SYSTEM_TO_PID,
127                                theSystem,
128                                t -> lookupResourceIdentifierSystemFromDatabase(theRequestDetails, theRequestPartitionId, theSystem));
129                return pid;
130        }
131
132        @Nonnull
133        @Override
134        public String getFhirIdAssociatedWithUniquePatientIdentifier(
135                        RequestDetails theRequestDetails,
136                        RequestPartitionId theRequestPartitionId,
137                        String theSystem,
138                        String theValue,
139                        Supplier<String> theNewIdSupplier) {
140
141                Validate.notBlank(theSystem, "theSystem must not be blank");
142                Validate.notBlank(theValue, "theValue must not be blank");
143
144                long identifierSystemPid =
145                                getOrCreateResourceIdentifierSystem(theRequestDetails, theRequestPartitionId, theSystem);
146                MemoryCacheService.IdentifierKey key = new MemoryCacheService.IdentifierKey(theSystem, theValue);
147                return myMemoryCache.getThenPutAfterCommit(
148                                MemoryCacheService.CacheEnum.PATIENT_IDENTIFIER_TO_FHIR_ID,
149                                key,
150                                t -> lookupResourceFhirIdForPatientIdentifier(
151                                                theRequestDetails, theRequestPartitionId, identifierSystemPid, theValue, theNewIdSupplier));
152        }
153
154        @Nullable
155        private String lookupResourceFhirIdForPatientIdentifier(
156                        RequestDetails theRequestDetails,
157                        RequestPartitionId theRequestPartitionId,
158                        long theSystem,
159                        String theValue,
160                        Supplier<String> theNewIdSupplier) {
161                return myTransactionService
162                                .withRequest(theRequestDetails)
163                                .withRequestPartitionId(theRequestPartitionId)
164                                .execute(() -> {
165                                        String retVal = myResourceIdentifierPatientUniqueEntityDao
166                                                        .findById(
167                                                                        new ResourceIdentifierPatientUniqueEntity.PatientIdentifierPk(theSystem, theValue))
168                                                        .map(ResourceIdentifierPatientUniqueEntity::getFhirId)
169                                                        .orElse(null);
170
171                                        if (retVal == null) {
172                                                retVal = theNewIdSupplier.get();
173                                                ourLog.trace("Created FHIR ID [{}] for SystemPid[{}] Value[{}]", retVal, theSystem, theValue);
174                                                ResourceIdentifierPatientUniqueEntity newEntity = new ResourceIdentifierPatientUniqueEntity();
175                                                newEntity.setPk(
176                                                                new ResourceIdentifierPatientUniqueEntity.PatientIdentifierPk(theSystem, theValue));
177                                                newEntity.setFhirId(retVal);
178                                                myEntityManager.persist(newEntity);
179                                        }
180
181                                        assert retVal != null;
182                                        return retVal;
183                                });
184        }
185
186        private Long lookupResourceIdentifierSystemFromDatabase(
187                        RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId, String theIdentifierSystem) {
188                return myTransactionService
189                                .withRequest(theRequestDetails)
190                                .withRequestPartitionId(theRequestPartitionId)
191                                .readOnly()
192                                .execute(() -> {
193                                        Long retVal = myResourceIdentifierSystemEntityDao
194                                                        .findBySystemUrl(theIdentifierSystem)
195                                                        .orElse(null);
196                                        ourLog.trace("Fetched PID[{}] for Identifier System: {}", retVal, theIdentifierSystem);
197                                        return retVal;
198                                });
199        }
200}