001package ca.uhn.fhir.jpa.migrate;
002
003/*-
004 * #%L
005 * HAPI FHIR Server - SQL Migration
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.context.ConfigurationException;
024import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask;
025import org.flywaydb.core.api.MigrationInfo;
026import org.flywaydb.core.api.MigrationInfoService;
027import org.flywaydb.core.api.callback.Callback;
028import org.hibernate.cfg.AvailableSettings;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031import org.springframework.util.Assert;
032
033import javax.sql.DataSource;
034import java.sql.Connection;
035import java.sql.SQLException;
036import java.util.Collections;
037import java.util.List;
038import java.util.Optional;
039import java.util.Properties;
040
041public class SchemaMigrator {
042        public static final String HAPI_FHIR_MIGRATION_TABLENAME = "FLY_HFJ_MIGRATION";
043        private static final Logger ourLog = LoggerFactory.getLogger(SchemaMigrator.class);
044        private final String mySchemaName;
045        private final DataSource myDataSource;
046        private final boolean mySkipValidation;
047        private final String myMigrationTableName;
048        private final List<BaseTask> myMigrationTasks;
049        private boolean myDontUseFlyway;
050        private boolean myStrictOrder;
051        private DriverTypeEnum myDriverType;
052        private List<Callback> myCallbacks = Collections.emptyList();
053
054        /**
055         * Constructor
056         */
057        public SchemaMigrator(String theSchemaName, String theMigrationTableName, DataSource theDataSource, Properties jpaProperties, List<BaseTask> theMigrationTasks) {
058                mySchemaName = theSchemaName;
059                myDataSource = theDataSource;
060                myMigrationTableName = theMigrationTableName;
061                myMigrationTasks = theMigrationTasks;
062
063                mySkipValidation = jpaProperties.containsKey(AvailableSettings.HBM2DDL_AUTO) && "update".equals(jpaProperties.getProperty(AvailableSettings.HBM2DDL_AUTO));
064        }
065
066        public void setCallbacks(List<Callback> theCallbacks) {
067                Assert.notNull(theCallbacks);
068                myCallbacks = theCallbacks;
069        }
070
071        public void setDontUseFlyway(boolean theDontUseFlyway) {
072                myDontUseFlyway = theDontUseFlyway;
073        }
074
075        public void setStrictOrder(boolean theStrictOrder) {
076                myStrictOrder = theStrictOrder;
077        }
078
079        public void validate() {
080                if (mySkipValidation) {
081                        ourLog.warn("Database running in hibernate auto-update mode.  Skipping schema validation.");
082                        return;
083                }
084                try (Connection connection = myDataSource.getConnection()) {
085                        Optional<MigrationInfoService> migrationInfo = newMigrator().getMigrationInfo();
086                        if (migrationInfo.isPresent()) {
087                                if (migrationInfo.get().pending().length > 0) {
088
089                                        String url = connection.getMetaData().getURL();
090                                        throw new ConfigurationException("The database schema for " + url + " is out of date.  " +
091                                                "Current database schema version is " + getCurrentVersion(migrationInfo.get()) + ".  Schema version required by application is " +
092                                                getLastVersion(migrationInfo.get()) + ".  Please run the database migrator.");
093                                }
094                                ourLog.info("Database schema confirmed at expected version " + getCurrentVersion(migrationInfo.get()));
095                        }
096                } catch (SQLException e) {
097                        throw new ConfigurationException("Unable to connect to " + myDataSource, e);
098                }
099        }
100
101        public void migrate() {
102                if (mySkipValidation) {
103                        ourLog.warn("Database running in hibernate auto-update mode.  Skipping schema migration.");
104                        return;
105                }
106                try {
107                        ourLog.info("Migrating " + mySchemaName);
108                        newMigrator().migrate();
109                        ourLog.info(mySchemaName + " migrated successfully.");
110                } catch (Exception e) {
111                        ourLog.error("Failed to migrate " + mySchemaName, e);
112                        throw e;
113                }
114        }
115
116        private BaseMigrator newMigrator() {
117                BaseMigrator migrator;
118                if (myDontUseFlyway) {
119                        migrator = new TaskOnlyMigrator();
120                        migrator.setDriverType(myDriverType);
121                        migrator.setDataSource(myDataSource);
122                } else {
123                        migrator = new FlywayMigrator(myMigrationTableName, myDataSource, myDriverType);
124                        migrator.setStrictOrder(myStrictOrder);
125                }
126                migrator.addTasks(myMigrationTasks);
127                migrator.setCallbacks(myCallbacks);
128                return migrator;
129        }
130
131        private String getCurrentVersion(MigrationInfoService theMigrationInfo) {
132                MigrationInfo migrationInfo = theMigrationInfo.current();
133                if (migrationInfo == null) {
134                        return "unknown";
135                }
136                return migrationInfo.getVersion().toString();
137        }
138
139        private String getLastVersion(MigrationInfoService theMigrationInfo) {
140                MigrationInfo[] pending = theMigrationInfo.pending();
141                if (pending.length > 0) {
142                        return pending[pending.length - 1].getVersion().toString();
143                }
144                return "unknown";
145        }
146
147        public void setDriverType(DriverTypeEnum theDriverType) {
148                myDriverType = theDriverType;
149        }
150}