
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}