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 invalidateCaches(); 207 } 208 209 @Override 210 public List<PartitionEntity> listPartitions() { 211 List<PartitionEntity> allPartitions = myPartitionDao.findAll(); 212 return allPartitions; 213 } 214 215 private void validatePartitionNameDoesntAlreadyExist(String theName) { 216 if (myPartitionDao.findForName(theName).isPresent()) { 217 String msg = myFhirCtx 218 .getLocalizer() 219 .getMessageSanitized(PartitionLookupSvcImpl.class, "cantCreateDuplicatePartitionName", theName); 220 throw new InvalidRequestException(Msg.code(1309) + msg); 221 } 222 } 223 224 private void validIdUponCreation(PartitionEntity thePartition) { 225 if (myPartitionDao.findById(thePartition.getId()).isPresent()) { 226 String msg = 227 myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "duplicatePartitionId"); 228 throw new InvalidRequestException(Msg.code(2366) + msg); 229 } 230 } 231 232 private void validateHaveValidPartitionIdAndName(PartitionEntity thePartition) { 233 if (thePartition.getId() == null || isBlank(thePartition.getName())) { 234 String msg = myFhirCtx.getLocalizer().getMessage(PartitionLookupSvcImpl.class, "missingPartitionIdOrName"); 235 throw new InvalidRequestException(Msg.code(1310) + msg); 236 } 237 238 if (thePartition.getName().equals(JpaConstants.DEFAULT_PARTITION_NAME)) { 239 String msg = myFhirCtx 240 .getLocalizer() 241 .getMessageSanitized(PartitionLookupSvcImpl.class, "cantCreateDefaultPartition"); 242 throw new InvalidRequestException(Msg.code(1311) + msg); 243 } 244 245 if (!PARTITION_NAME_VALID_PATTERN.matcher(thePartition.getName()).matches()) { 246 String msg = myFhirCtx 247 .getLocalizer() 248 .getMessageSanitized(PartitionLookupSvcImpl.class, "invalidName", thePartition.getName()); 249 throw new InvalidRequestException(Msg.code(1312) + msg); 250 } 251 } 252 253 private void validateNotInUnnamedPartitionMode() { 254 if (myPartitionSettings.isUnnamedPartitionMode()) { 255 throw new MethodNotAllowedException( 256 Msg.code(1313) + "Can not invoke this operation in unnamed partition mode"); 257 } 258 } 259 260 private PartitionEntity lookupPartitionByName(@Nonnull String theName) { 261 return executeInTransaction(() -> myPartitionDao.findForName(theName)).orElseThrow(() -> { 262 String msg = 263 myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "invalidName", theName); 264 return new ResourceNotFoundException(msg); 265 }); 266 } 267 268 private PartitionEntity lookupPartitionById(@Nonnull Integer theId) { 269 try { 270 return executeInTransaction(() -> myPartitionDao.findById(theId)).orElseThrow(() -> { 271 String msg = myFhirCtx 272 .getLocalizer() 273 .getMessageSanitized(PartitionLookupSvcImpl.class, "unknownPartitionId", theId); 274 return new ResourceNotFoundException(msg); 275 }); 276 } catch (ResourceNotFoundException e) { 277 List<PartitionEntity> allPartitions = executeInTransaction(() -> myPartitionDao.findAll()); 278 String allPartitionsString = allPartitions.stream() 279 .map(t -> t.getId() + "/" + t.getName()) 280 .collect(Collectors.joining(", ")); 281 ourLog.warn("Failed to find partition with ID {}. Current partitions: {}", theId, allPartitionsString); 282 throw e; 283 } 284 } 285 286 protected <T> T executeInTransaction(ICallable<T> theCallable) { 287 return myTxTemplate.execute(tx -> theCallable.call()); 288 } 289 290 public static void validatePartitionIdSupplied(FhirContext theFhirContext, Integer thePartitionId) { 291 if (thePartitionId == null) { 292 String msg = 293 theFhirContext.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "noIdSupplied"); 294 throw new InvalidRequestException(Msg.code(1314) + msg); 295 } 296 } 297}