
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}