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}