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}