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