001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 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.partition; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.HookParams; 025import ca.uhn.fhir.interceptor.api.IInterceptorService; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.interceptor.model.RequestPartitionId; 028import ca.uhn.fhir.jpa.dao.data.IPartitionDao; 029import ca.uhn.fhir.jpa.entity.PartitionEntity; 030import ca.uhn.fhir.jpa.model.config.PartitionSettings; 031import ca.uhn.fhir.jpa.model.util.JpaConstants; 032import ca.uhn.fhir.jpa.util.MemoryCacheService; 033import ca.uhn.fhir.rest.api.server.RequestDetails; 034import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 035import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 036import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 037import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 038import ca.uhn.fhir.util.ICallable; 039import jakarta.annotation.Nonnull; 040import jakarta.annotation.PostConstruct; 041import org.apache.commons.lang3.Validate; 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044import org.springframework.beans.factory.annotation.Autowired; 045import org.springframework.transaction.PlatformTransactionManager; 046import org.springframework.transaction.annotation.Transactional; 047import org.springframework.transaction.support.TransactionTemplate; 048 049import java.util.List; 050import java.util.Optional; 051import java.util.concurrent.ThreadLocalRandom; 052import java.util.regex.Pattern; 053import java.util.stream.Collectors; 054 055import static org.apache.commons.lang3.StringUtils.isBlank; 056 057public class PartitionLookupSvcImpl implements IPartitionLookupSvc { 058 059 private static final Pattern PARTITION_NAME_VALID_PATTERN = Pattern.compile("[a-zA-Z0-9_-]+"); 060 private static final Logger ourLog = LoggerFactory.getLogger(PartitionLookupSvcImpl.class); 061 062 @Autowired 063 private PartitionSettings myPartitionSettings; 064 065 @Autowired 066 private IInterceptorService myInterceptorService; 067 068 @Autowired 069 private IPartitionDao myPartitionDao; 070 071 @Autowired 072 private MemoryCacheService myMemoryCacheService; 073 074 @Autowired 075 private FhirContext myFhirCtx; 076 077 @Autowired 078 private PlatformTransactionManager myTxManager; 079 080 private TransactionTemplate myTxTemplate; 081 082 /** 083 * Constructor 084 */ 085 public PartitionLookupSvcImpl() { 086 super(); 087 } 088 089 @Override 090 @PostConstruct 091 public void start() { 092 myTxTemplate = new TransactionTemplate(myTxManager); 093 } 094 095 @Override 096 public PartitionEntity getPartitionByName(String theName) { 097 Validate.notBlank(theName, "The name must not be null or blank"); 098 validateNotInUnnamedPartitionMode(); 099 if (JpaConstants.DEFAULT_PARTITION_NAME.equals(theName)) { 100 return null; 101 } 102 return myMemoryCacheService.get( 103 MemoryCacheService.CacheEnum.NAME_TO_PARTITION, theName, this::lookupPartitionByName); 104 } 105 106 @Override 107 public PartitionEntity getPartitionById(Integer thePartitionId) { 108 validatePartitionIdSupplied(myFhirCtx, thePartitionId); 109 if (myPartitionSettings.isUnnamedPartitionMode()) { 110 return new PartitionEntity().setId(thePartitionId); 111 } 112 if (myPartitionSettings.getDefaultPartitionId() != null 113 && myPartitionSettings.getDefaultPartitionId().equals(thePartitionId)) { 114 return new PartitionEntity().setId(thePartitionId).setName(JpaConstants.DEFAULT_PARTITION_NAME); 115 } 116 return myMemoryCacheService.get( 117 MemoryCacheService.CacheEnum.ID_TO_PARTITION, thePartitionId, this::lookupPartitionById); 118 } 119 120 @Override 121 public void invalidateCaches() { 122 myMemoryCacheService.invalidateCaches( 123 MemoryCacheService.CacheEnum.NAME_TO_PARTITION, MemoryCacheService.CacheEnum.ID_TO_PARTITION); 124 } 125 126 /** 127 * Generate a random postive integer between 1 and Integer.MAX_VALUE, which is guaranteed to be unused by an existing partition. 128 * @return an integer representing a partition ID that is not currently in use by the system. 129 */ 130 public int generateRandomUnusedPartitionId() { 131 int candidate = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE); 132 Optional<PartitionEntity> partition = myPartitionDao.findById(candidate); 133 while (partition.isPresent()) { 134 candidate = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE); 135 partition = myPartitionDao.findById(candidate); 136 } 137 return candidate; 138 } 139 140 @Override 141 @Transactional 142 public PartitionEntity createPartition(PartitionEntity thePartition, RequestDetails theRequestDetails) { 143 144 validateNotInUnnamedPartitionMode(); 145 validateHaveValidPartitionIdAndName(thePartition); 146 validatePartitionNameDoesntAlreadyExist(thePartition.getName()); 147 validIdUponCreation(thePartition); 148 ourLog.info("Creating new partition with ID {} and Name {}", thePartition.getId(), thePartition.getName()); 149 150 PartitionEntity retVal = myPartitionDao.save(thePartition); 151 152 // Interceptor call: STORAGE_PARTITION_CREATED 153 if (myInterceptorService.hasHooks(Pointcut.STORAGE_PARTITION_CREATED)) { 154 HookParams params = new HookParams() 155 .add(RequestPartitionId.class, thePartition.toRequestPartitionId()) 156 .add(RequestDetails.class, theRequestDetails) 157 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); 158 myInterceptorService.callHooks(Pointcut.STORAGE_PARTITION_CREATED, params); 159 } 160 161 return retVal; 162 } 163 164 @Override 165 @Transactional 166 public PartitionEntity updatePartition(PartitionEntity thePartition) { 167 validateNotInUnnamedPartitionMode(); 168 validateHaveValidPartitionIdAndName(thePartition); 169 170 Optional<PartitionEntity> existingPartitionOpt = myPartitionDao.findById(thePartition.getId()); 171 if (existingPartitionOpt.isPresent() == false) { 172 String msg = myFhirCtx 173 .getLocalizer() 174 .getMessageSanitized(PartitionLookupSvcImpl.class, "unknownPartitionId", thePartition.getId()); 175 throw new InvalidRequestException(Msg.code(1307) + msg); 176 } 177 178 PartitionEntity existingPartition = existingPartitionOpt.get(); 179 if (!thePartition.getName().equalsIgnoreCase(existingPartition.getName())) { 180 validatePartitionNameDoesntAlreadyExist(thePartition.getName()); 181 } 182 183 existingPartition.setName(thePartition.getName()); 184 existingPartition.setDescription(thePartition.getDescription()); 185 myPartitionDao.save(existingPartition); 186 invalidateCaches(); 187 return existingPartition; 188 } 189 190 @Override 191 @Transactional 192 public void deletePartition(Integer thePartitionId) { 193 validatePartitionIdSupplied(myFhirCtx, thePartitionId); 194 validateNotInUnnamedPartitionMode(); 195 196 Optional<PartitionEntity> partition = myPartitionDao.findById(thePartitionId); 197 if (!partition.isPresent()) { 198 String msg = myFhirCtx 199 .getLocalizer() 200 .getMessageSanitized(PartitionLookupSvcImpl.class, "unknownPartitionId", thePartitionId); 201 throw new IllegalArgumentException(Msg.code(1308) + msg); 202 } 203 204 myPartitionDao.delete(partition.get()); 205 206 if (myInterceptorService.hasHooks(Pointcut.STORAGE_PARTITION_DELETED)) { 207 HookParams params = new HookParams() 208 .add(RequestPartitionId.class, partition.get().toRequestPartitionId()); 209 myInterceptorService.callHooks(Pointcut.STORAGE_PARTITION_DELETED, params); 210 } 211 212 invalidateCaches(); 213 } 214 215 @Override 216 public List<PartitionEntity> listPartitions() { 217 List<PartitionEntity> allPartitions = myPartitionDao.findAll(); 218 return allPartitions; 219 } 220 221 private void validatePartitionNameDoesntAlreadyExist(String theName) { 222 if (myPartitionDao.findForName(theName).isPresent()) { 223 String msg = myFhirCtx 224 .getLocalizer() 225 .getMessageSanitized(PartitionLookupSvcImpl.class, "cantCreateDuplicatePartitionName", theName); 226 throw new InvalidRequestException(Msg.code(1309) + msg); 227 } 228 } 229 230 private void validIdUponCreation(PartitionEntity thePartition) { 231 if (myPartitionDao.findById(thePartition.getId()).isPresent()) { 232 String msg = 233 myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "duplicatePartitionId"); 234 throw new InvalidRequestException(Msg.code(2366) + msg); 235 } 236 } 237 238 private void validateHaveValidPartitionIdAndName(PartitionEntity thePartition) { 239 if (thePartition.getId() == null || isBlank(thePartition.getName())) { 240 String msg = myFhirCtx.getLocalizer().getMessage(PartitionLookupSvcImpl.class, "missingPartitionIdOrName"); 241 throw new InvalidRequestException(Msg.code(1310) + msg); 242 } 243 244 if (thePartition.getName().equals(JpaConstants.DEFAULT_PARTITION_NAME)) { 245 String msg = myFhirCtx 246 .getLocalizer() 247 .getMessageSanitized(PartitionLookupSvcImpl.class, "cantCreateDefaultPartition"); 248 throw new InvalidRequestException(Msg.code(1311) + msg); 249 } 250 251 if (!PARTITION_NAME_VALID_PATTERN.matcher(thePartition.getName()).matches()) { 252 String msg = myFhirCtx 253 .getLocalizer() 254 .getMessageSanitized(PartitionLookupSvcImpl.class, "invalidName", thePartition.getName()); 255 throw new InvalidRequestException(Msg.code(1312) + msg); 256 } 257 } 258 259 private void validateNotInUnnamedPartitionMode() { 260 if (myPartitionSettings.isUnnamedPartitionMode()) { 261 throw new MethodNotAllowedException( 262 Msg.code(1313) + "Can not invoke this operation in unnamed partition mode"); 263 } 264 } 265 266 private PartitionEntity lookupPartitionByName(@Nonnull String theName) { 267 return executeInTransaction(() -> myPartitionDao.findForName(theName)).orElseThrow(() -> { 268 String msg = 269 myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "invalidName", theName); 270 return new ResourceNotFoundException(msg); 271 }); 272 } 273 274 private PartitionEntity lookupPartitionById(@Nonnull Integer theId) { 275 try { 276 return executeInTransaction(() -> myPartitionDao.findById(theId)).orElseThrow(() -> { 277 String msg = myFhirCtx 278 .getLocalizer() 279 .getMessageSanitized(PartitionLookupSvcImpl.class, "unknownPartitionId", theId); 280 return new ResourceNotFoundException(msg); 281 }); 282 } catch (ResourceNotFoundException e) { 283 List<PartitionEntity> allPartitions = executeInTransaction(() -> myPartitionDao.findAll()); 284 String allPartitionsString = allPartitions.stream() 285 .map(t -> t.getId() + "/" + t.getName()) 286 .collect(Collectors.joining(", ")); 287 ourLog.warn("Failed to find partition with ID {}. Current partitions: {}", theId, allPartitionsString); 288 throw e; 289 } 290 } 291 292 protected <T> T executeInTransaction(ICallable<T> theCallable) { 293 return myTxTemplate.execute(tx -> theCallable.call()); 294 } 295 296 public static void validatePartitionIdSupplied(FhirContext theFhirContext, Integer thePartitionId) { 297 if (thePartitionId == null) { 298 String msg = 299 theFhirContext.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "noIdSupplied"); 300 throw new InvalidRequestException(Msg.code(1314) + msg); 301 } 302 } 303}