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}