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}