001/*-
002 * #%L
003 * HAPI FHIR Server - SQL Migration
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.migrate;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.i18n.Msg;
024import org.hibernate.cfg.AvailableSettings;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import java.sql.Connection;
029import java.sql.SQLException;
030import java.util.Collections;
031import java.util.List;
032import java.util.Properties;
033import javax.sql.DataSource;
034
035/**
036 * The SchemaMigrator class is responsible for managing and executing database schema migrations during standard bootup.
037 * It provides methods to validate the schema version and migrate the schema if necessary.
038 *
039 * This supports all the configuation needed to run via the CLI or via a bootup migrator. Specifically
040 * 1. Dry Run
041 * 2. Enable Heavyweight Tasks
042 * 3. Task Skipping
043 *
044 */
045public class SchemaMigrator {
046        public static final String HAPI_FHIR_MIGRATION_TABLENAME = "FLY_HFJ_MIGRATION";
047        private static final Logger ourLog = LoggerFactory.getLogger(SchemaMigrator.class);
048        /**
049         * The Schema Name
050         */
051        private final String mySchemaName;
052        /**
053         * The datasource to connect to the Database with.
054         */
055        private final DataSource myDataSource;
056        /**
057         * Whether to skip validation of the schema
058         */
059        private final boolean mySkipValidation;
060        /**
061         * See {@link HapiMigrator#isRunHeavyweightSkippableTasks()}. Enables or disables Optional Heavyweight migrations
062         */
063        private final boolean myRunHeavyweightMigrationTasks;
064        /**
065         * The name of the table in which migrations are tracked.
066         */
067        private final String myMigrationTableName;
068        /**
069         * The actual task list, which will be filtered during construction, and having all Tasks enumerated in `theMigrationTasksToSkip` removed before execution.
070         */
071        private final MigrationTaskList myMigrationTasks;
072        /**
073         * See {@link HapiMigrator#isDryRun()}
074         */
075        private final boolean myDryRun;
076
077        private DriverTypeEnum myDriverType;
078        /**
079         * Inventory of callbacks to invoke for pre and post execution
080         */
081        private List<IHapiMigrationCallback> myCallbacks = Collections.emptyList();
082
083        private final HapiMigrationStorageSvc myHapiMigrationStorageSvc;
084
085        /**
086         * Constructor
087         */
088        public SchemaMigrator(
089                        String theSchemaName,
090                        String theMigrationTableName,
091                        DataSource theDataSource,
092                        boolean theEnableHeavyweighTtasks,
093                        String theMigrationTasksToSkip,
094                        boolean theDryRun,
095                        Properties jpaProperties,
096                        MigrationTaskList theMigrationTasks,
097                        HapiMigrationStorageSvc theHapiMigrationStorageSvc) {
098                mySchemaName = theSchemaName;
099                myDataSource = theDataSource;
100                myMigrationTableName = theMigrationTableName;
101                myMigrationTasks = theMigrationTasks;
102                myRunHeavyweightMigrationTasks = theEnableHeavyweighTtasks;
103                mySkipValidation = jpaProperties.containsKey(AvailableSettings.HBM2DDL_AUTO)
104                                && "update".equals(jpaProperties.getProperty(AvailableSettings.HBM2DDL_AUTO));
105                myHapiMigrationStorageSvc = theHapiMigrationStorageSvc;
106                // Skip the skipped versions here.
107                myMigrationTasks.setDoNothingOnSkippedTasks(theMigrationTasksToSkip);
108                myDryRun = theDryRun;
109        }
110
111        /**
112         * Temporary Dummy Constructor to remove once CDR side merges.
113         */
114        public SchemaMigrator(
115                        String theSchemaName,
116                        String theMigrationTableName,
117                        DataSource theDataSource,
118                        Properties jpaProperties,
119                        MigrationTaskList theMigrationTasks,
120                        HapiMigrationStorageSvc theHapiMigrationStorageSvc) {
121                mySchemaName = theSchemaName;
122                myDataSource = theDataSource;
123                myMigrationTableName = theMigrationTableName;
124                myMigrationTasks = theMigrationTasks;
125                myRunHeavyweightMigrationTasks = false;
126                mySkipValidation = jpaProperties.containsKey(AvailableSettings.HBM2DDL_AUTO)
127                                && "update".equals(jpaProperties.getProperty(AvailableSettings.HBM2DDL_AUTO));
128                myHapiMigrationStorageSvc = theHapiMigrationStorageSvc;
129                // Skip the skipped versions here.
130                myDryRun = false;
131        }
132
133        public void validate() {
134                if (mySkipValidation) {
135                        ourLog.warn("Database running in hibernate auto-update mode.  Skipping schema validation.");
136                        return;
137                }
138                try (Connection connection = myDataSource.getConnection()) {
139                        MigrationTaskList unappliedMigrations = myHapiMigrationStorageSvc.diff(myMigrationTasks);
140
141                        // remove skippable tasks
142                        MigrationTaskList unappliedUnskippable = unappliedMigrations.getUnskippableTasks();
143
144                        if (unappliedUnskippable.size() > 0) {
145                                String url = connection.getMetaData().getURL();
146                                throw new ConfigurationException(Msg.code(27) + "The database schema for " + url + " is out of date.  "
147                                                + "Current database schema version is "
148                                                + myHapiMigrationStorageSvc.getLatestAppliedVersion()
149                                                + ".  Schema version required by application is " + unappliedMigrations.getLastVersion()
150                                                + ".  Please run the database migrator.");
151                        }
152                        ourLog.info("Database schema confirmed at expected version "
153                                        + myHapiMigrationStorageSvc.getLatestAppliedVersion());
154                } catch (SQLException e) {
155                        throw new ConfigurationException(Msg.code(28) + "Unable to connect to " + myDataSource, e);
156                }
157        }
158
159        public MigrationResult migrate() {
160                if (mySkipValidation) {
161                        ourLog.warn("Database running in hibernate auto-update mode.  Skipping schema migration.");
162                        return null;
163                }
164                try {
165                        ourLog.info("Migrating " + mySchemaName);
166                        MigrationResult retval = newMigrator().migrate();
167                        ourLog.info(mySchemaName + " migrated successfully: {}", retval.summary());
168                        return retval;
169                } catch (Exception e) {
170                        ourLog.error("Failed to migrate " + mySchemaName, e);
171                        throw e;
172                }
173        }
174
175        private HapiMigrator newMigrator() {
176                HapiMigrator migrator;
177                migrator = new HapiMigrator(myMigrationTableName, myDataSource, myDriverType);
178                migrator.addTasks(myMigrationTasks);
179                migrator.setCallbacks(myCallbacks);
180                migrator.setDryRun(myDryRun);
181                migrator.setRunHeavyweightSkippableTasks(myRunHeavyweightMigrationTasks);
182                return migrator;
183        }
184
185        public void setDriverType(DriverTypeEnum theDriverType) {
186                myDriverType = theDriverType;
187        }
188
189        public void setCallbacks(List<IHapiMigrationCallback> theCallbacks) {
190                myCallbacks = theCallbacks;
191        }
192
193        public boolean createMigrationTableIfRequired() {
194                return myHapiMigrationStorageSvc.createMigrationTableIfRequired();
195        }
196}