001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2025 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.util;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
025import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
026import ca.uhn.fhir.jpa.migrate.JdbcUtils;
027import ca.uhn.fhir.jpa.model.config.PartitionSettings;
028import ca.uhn.fhir.jpa.model.dialect.IHapiFhirDialect;
029import ca.uhn.fhir.system.HapiSystemProperties;
030import org.apache.commons.collections4.SetUtils;
031import org.hibernate.dialect.Dialect;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034import org.springframework.context.event.ContextRefreshedEvent;
035import org.springframework.context.event.EventListener;
036import org.springframework.transaction.PlatformTransactionManager;
037import org.springframework.transaction.support.TransactionTemplate;
038
039import java.sql.SQLException;
040import java.util.Set;
041import java.util.TreeSet;
042import javax.sql.DataSource;
043
044/**
045 * This bean simply performs a startup check that the database schema is
046 * appropriate for the Partitioned ID mode set in configuration.
047 * If partitioned ID mode is active then the PARTITION_ID column
048 * should be a part of primary keys.
049 */
050public class PartitionedIdModeVerificationSvc {
051
052        private static final Logger ourLog = LoggerFactory.getLogger(PartitionedIdModeVerificationSvc.class);
053
054        private final PartitionSettings myPartitionSettings;
055        private final HibernatePropertiesProvider myHibernatePropertiesProvider;
056        private final PlatformTransactionManager myTxManager;
057
058        /**
059         * Constructor
060         */
061        public PartitionedIdModeVerificationSvc(
062                        PartitionSettings thePartitionSettings,
063                        HibernatePropertiesProvider theHibernatePropertiesProvider,
064                        PlatformTransactionManager theTxManager) {
065                myPartitionSettings = thePartitionSettings;
066                myHibernatePropertiesProvider = theHibernatePropertiesProvider;
067                myTxManager = theTxManager;
068        }
069
070        @EventListener(classes = {ContextRefreshedEvent.class})
071        public void verifyPartitionedIdMode() throws SQLException {
072
073                DataSource dataSource = myHibernatePropertiesProvider.getDataSource();
074                boolean expectDatabasePartitionMode = myPartitionSettings.isDatabasePartitionMode();
075
076                Dialect dialect = myHibernatePropertiesProvider.getDialect();
077                if (!(dialect instanceof IHapiFhirDialect)) {
078                        ourLog.warn("Dialect is not a HAPI FHIR dialect: {}", dialect);
079                        return;
080                }
081
082                DriverTypeEnum driverType = ((IHapiFhirDialect) dialect).getDriverType();
083                TransactionTemplate transactionTemplate = new TransactionTemplate(myTxManager);
084                DriverTypeEnum.ConnectionProperties cp =
085                                new DriverTypeEnum.ConnectionProperties(dataSource, transactionTemplate, driverType);
086                verifySchemaIsAppropriateForDatabasePartitionMode(cp, expectDatabasePartitionMode);
087        }
088
089        public static void verifySchemaIsAppropriateForDatabasePartitionMode(
090                        DriverTypeEnum.ConnectionProperties cp, boolean expectDatabasePartitionMode) throws SQLException {
091
092                if (HapiSystemProperties.isDisableDatabasePartitionModeSchemaCheck()) {
093                        return;
094                }
095
096                Set<String> pkColumns = JdbcUtils.getPrimaryKeyColumns(cp, "HFJ_RESOURCE");
097                if (pkColumns.isEmpty()) {
098                        return;
099                }
100
101                if (!expectDatabasePartitionMode) {
102                        if (!SetUtils.isEqualSet(pkColumns, Set.of("RES_ID"))) {
103                                throw new ConfigurationException(Msg.code(2563)
104                                                + "System is configured in Partitioned ID mode but the database schema is not correct for this. Found HFJ_RESOURCE PK: "
105                                                + new TreeSet<>(pkColumns));
106                        }
107                } else {
108                        if (!SetUtils.isEqualSet(pkColumns, Set.of("RES_ID", "PARTITION_ID"))) {
109                                throw new ConfigurationException(Msg.code(2564)
110                                                + "System is configured in Partitioned ID mode but the database schema is not correct for this. Found HFJ_RESOURCE PK: "
111                                                + new TreeSet<>(pkColumns));
112                        }
113                }
114        }
115}