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}