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