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