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}