
001package ca.uhn.fhir.jpa.partition; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.jpa.dao.data.IPartitionDao; 026import ca.uhn.fhir.jpa.entity.PartitionEntity; 027import ca.uhn.fhir.jpa.model.config.PartitionSettings; 028import ca.uhn.fhir.jpa.model.util.JpaConstants; 029import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 030import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 031import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 032import com.github.benmanes.caffeine.cache.CacheLoader; 033import com.github.benmanes.caffeine.cache.Caffeine; 034import com.github.benmanes.caffeine.cache.LoadingCache; 035import org.apache.commons.lang3.Validate; 036import org.checkerframework.checker.nullness.qual.NonNull; 037import org.checkerframework.checker.nullness.qual.Nullable; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040import org.springframework.beans.factory.annotation.Autowired; 041import org.springframework.transaction.PlatformTransactionManager; 042import org.springframework.transaction.support.TransactionTemplate; 043 044import javax.annotation.PostConstruct; 045import javax.transaction.Transactional; 046import java.util.List; 047import java.util.Optional; 048import java.util.concurrent.TimeUnit; 049import java.util.regex.Pattern; 050 051import static org.apache.commons.lang3.StringUtils.isBlank; 052 053public class PartitionLookupSvcImpl implements IPartitionLookupSvc { 054 055 private static final Pattern PARTITION_NAME_VALID_PATTERN = Pattern.compile("[a-zA-Z0-9_-]+"); 056 private static final Logger ourLog = LoggerFactory.getLogger(PartitionLookupSvcImpl.class); 057 058 @Autowired 059 private PartitionSettings myPartitionSettings; 060 @Autowired 061 private PlatformTransactionManager myTxManager; 062 @Autowired 063 private IPartitionDao myPartitionDao; 064 065 private LoadingCache<String, PartitionEntity> myNameToPartitionCache; 066 private LoadingCache<Integer, PartitionEntity> myIdToPartitionCache; 067 private TransactionTemplate myTxTemplate; 068 @Autowired 069 private FhirContext myFhirCtx; 070 071 /** 072 * Constructor 073 */ 074 public PartitionLookupSvcImpl() { 075 super(); 076 } 077 078 @Override 079 @PostConstruct 080 public void start() { 081 myNameToPartitionCache = Caffeine 082 .newBuilder() 083 .expireAfterWrite(1, TimeUnit.MINUTES) 084 .build(new NameToPartitionCacheLoader()); 085 myIdToPartitionCache = Caffeine 086 .newBuilder() 087 .expireAfterWrite(1, TimeUnit.MINUTES) 088 .build(new IdToPartitionCacheLoader()); 089 myTxTemplate = new TransactionTemplate(myTxManager); 090 } 091 092 @Override 093 public PartitionEntity getPartitionByName(String theName) { 094 Validate.notBlank(theName, "The name must not be null or blank"); 095 validateNotInUnnamedPartitionMode(); 096 if (JpaConstants.DEFAULT_PARTITION_NAME.equals(theName)) { 097 return null; 098 } 099 return myNameToPartitionCache.get(theName); 100 } 101 102 @Override 103 public PartitionEntity getPartitionById(Integer thePartitionId) { 104 validatePartitionIdSupplied(myFhirCtx, thePartitionId); 105 if (myPartitionSettings.isUnnamedPartitionMode()) { 106 return new PartitionEntity().setId(thePartitionId); 107 } 108 if (myPartitionSettings.getDefaultPartitionId() != null && myPartitionSettings.getDefaultPartitionId().equals(thePartitionId)) { 109 return new PartitionEntity().setId(thePartitionId).setName(JpaConstants.DEFAULT_PARTITION_NAME); 110 } 111 return myIdToPartitionCache.get(thePartitionId); 112 } 113 114 @Override 115 public void clearCaches() { 116 myNameToPartitionCache.invalidateAll(); 117 myIdToPartitionCache.invalidateAll(); 118 } 119 120 @Override 121 @Transactional 122 public PartitionEntity createPartition(PartitionEntity thePartition) { 123 validateNotInUnnamedPartitionMode(); 124 validateHaveValidPartitionIdAndName(thePartition); 125 validatePartitionNameDoesntAlreadyExist(thePartition.getName()); 126 127 ourLog.info("Creating new partition with ID {} and Name {}", thePartition.getId(), thePartition.getName()); 128 129 myPartitionDao.save(thePartition); 130 return thePartition; 131 } 132 133 @Override 134 @Transactional 135 public PartitionEntity updatePartition(PartitionEntity thePartition) { 136 validateNotInUnnamedPartitionMode(); 137 validateHaveValidPartitionIdAndName(thePartition); 138 139 Optional<PartitionEntity> existingPartitionOpt = myPartitionDao.findById(thePartition.getId()); 140 if (existingPartitionOpt.isPresent() == false) { 141 String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "unknownPartitionId", thePartition.getId()); 142 throw new InvalidRequestException(Msg.code(1307) + msg); 143 } 144 145 PartitionEntity existingPartition = existingPartitionOpt.get(); 146 if (!thePartition.getName().equalsIgnoreCase(existingPartition.getName())) { 147 validatePartitionNameDoesntAlreadyExist(thePartition.getName()); 148 } 149 150 existingPartition.setName(thePartition.getName()); 151 existingPartition.setDescription(thePartition.getDescription()); 152 myPartitionDao.save(existingPartition); 153 clearCaches(); 154 return existingPartition; 155 } 156 157 @Override 158 @Transactional 159 public void deletePartition(Integer thePartitionId) { 160 validatePartitionIdSupplied(myFhirCtx, thePartitionId); 161 validateNotInUnnamedPartitionMode(); 162 163 Optional<PartitionEntity> partition = myPartitionDao.findById(thePartitionId); 164 if (!partition.isPresent()) { 165 String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "unknownPartitionId", thePartitionId); 166 throw new IllegalArgumentException(Msg.code(1308) + msg); 167 } 168 169 myPartitionDao.delete(partition.get()); 170 171 clearCaches(); 172 } 173 174 @Override 175 public List<PartitionEntity> listPartitions() { 176 List<PartitionEntity> allPartitions = myPartitionDao.findAll(); 177 return allPartitions; 178 } 179 180 private void validatePartitionNameDoesntAlreadyExist(String theName) { 181 if (myPartitionDao.findForName(theName).isPresent()) { 182 String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "cantCreateDuplicatePartitionName", theName); 183 throw new InvalidRequestException(Msg.code(1309) + msg); 184 } 185 } 186 187 private void validateHaveValidPartitionIdAndName(PartitionEntity thePartition) { 188 if (thePartition.getId() == null || isBlank(thePartition.getName())) { 189 String msg = myFhirCtx.getLocalizer().getMessage(PartitionLookupSvcImpl.class, "missingPartitionIdOrName"); 190 throw new InvalidRequestException(Msg.code(1310) + msg); 191 } 192 193 if (thePartition.getName().equals(JpaConstants.DEFAULT_PARTITION_NAME)) { 194 String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "cantCreateDefaultPartition"); 195 throw new InvalidRequestException(Msg.code(1311) + msg); 196 } 197 198 if (!PARTITION_NAME_VALID_PATTERN.matcher(thePartition.getName()).matches()) { 199 String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "invalidName", thePartition.getName()); 200 throw new InvalidRequestException(Msg.code(1312) + msg); 201 } 202 203 } 204 205 private void validateNotInUnnamedPartitionMode() { 206 if (myPartitionSettings.isUnnamedPartitionMode()) { 207 throw new MethodNotAllowedException(Msg.code(1313) + "Can not invoke this operation in unnamed partition mode"); 208 } 209 } 210 211 private class NameToPartitionCacheLoader implements @NonNull CacheLoader<String, PartitionEntity> { 212 @Nullable 213 @Override 214 public PartitionEntity load(@NonNull String theName) { 215 return myTxTemplate.execute(t -> myPartitionDao 216 .findForName(theName) 217 .orElseThrow(() -> { 218 String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "invalidName", theName); 219 return new ResourceNotFoundException(msg); 220 })); 221 } 222 } 223 224 private class IdToPartitionCacheLoader implements @NonNull CacheLoader<Integer, PartitionEntity> { 225 @Nullable 226 @Override 227 public PartitionEntity load(@NonNull Integer theId) { 228 return myTxTemplate.execute(t -> myPartitionDao 229 .findById(theId) 230 .orElseThrow(() -> { 231 String msg = myFhirCtx.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "unknownPartitionId", theId); 232 return new ResourceNotFoundException(msg); 233 })); 234 } 235 } 236 237 public static void validatePartitionIdSupplied(FhirContext theFhirContext, Integer thePartitionId) { 238 if (thePartitionId == null) { 239 String msg = theFhirContext.getLocalizer().getMessageSanitized(PartitionLookupSvcImpl.class, "noIdSupplied"); 240 throw new InvalidRequestException(Msg.code(1314) + msg); 241 } 242 } 243}