001package ca.uhn.fhir.jpa.migrate;
002
003/*-
004 * #%L
005 * HAPI FHIR Server - SQL Migration
006 * %%
007 * Copyright (C) 2014 - 2023 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.jpa.migrate.dao.HapiMigrationDao;
025import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask;
026import ca.uhn.fhir.jpa.migrate.taskdef.InitializeSchemaTask;
027import ca.uhn.fhir.system.HapiSystemProperties;
028import ca.uhn.fhir.util.StopWatch;
029import com.google.common.annotations.VisibleForTesting;
030import org.apache.commons.lang3.Validate;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import javax.annotation.Nonnull;
035import javax.sql.DataSource;
036import java.sql.SQLException;
037import java.util.Collections;
038import java.util.List;
039import java.util.Objects;
040
041import static org.apache.commons.lang3.StringUtils.isBlank;
042
043public class HapiMigrator {
044
045        private static final Logger ourLog = LoggerFactory.getLogger(HapiMigrator.class);
046        private final MigrationTaskList myTaskList = new MigrationTaskList();
047        private boolean myDryRun;
048        private boolean myNoColumnShrink;
049        private final DriverTypeEnum myDriverType;
050        private final DataSource myDataSource;
051        private final HapiMigrationStorageSvc myHapiMigrationStorageSvc;
052        private List<IHapiMigrationCallback> myCallbacks = Collections.emptyList();
053
054        public HapiMigrator(String theMigrationTableName, DataSource theDataSource, DriverTypeEnum theDriverType) {
055                myDriverType = theDriverType;
056                myDataSource = theDataSource;
057                myHapiMigrationStorageSvc = new HapiMigrationStorageSvc(new HapiMigrationDao(theDataSource, theDriverType, theMigrationTableName));
058        }
059
060        public DataSource getDataSource() {
061                return myDataSource;
062        }
063
064        public boolean isDryRun() {
065                return myDryRun;
066        }
067
068        public void setDryRun(boolean theDryRun) {
069                myDryRun = theDryRun;
070        }
071
072        public boolean isNoColumnShrink() {
073                return myNoColumnShrink;
074        }
075
076        public void setNoColumnShrink(boolean theNoColumnShrink) {
077                myNoColumnShrink = theNoColumnShrink;
078        }
079
080        public DriverTypeEnum getDriverType() {
081                return myDriverType;
082        }
083
084
085        protected StringBuilder buildExecutedStatementsString(MigrationResult theMigrationResult) {
086                StringBuilder statementBuilder = new StringBuilder();
087                String lastTable = null;
088                for (BaseTask.ExecutedStatement next : theMigrationResult.executedStatements) {
089                        if (!Objects.equals(lastTable, next.getTableName())) {
090                                statementBuilder.append("\n\n-- Table: ").append(next.getTableName()).append("\n");
091                                lastTable = next.getTableName();
092                        }
093
094                        statementBuilder.append(next.getSql()).append(";\n");
095
096                        for (Object nextArg : next.getArguments()) {
097                                statementBuilder.append("  -- Arg: ").append(nextArg).append("\n");
098                        }
099                }
100                return statementBuilder;
101        }
102
103        /**
104         * Helper method to clear a lock with a given UUID.
105         * @param theUUID the
106         */
107        public void clearMigrationLockWithUUID(String theUUID) {
108                ourLog.info("Attempting to remove lock entry. [uuid={}]", theUUID);
109                boolean success = myHapiMigrationStorageSvc.deleteLockRecord(theUUID);
110                if (success) {
111                        ourLog.info("Successfully removed lock entry. [uuid={}]", theUUID);
112                } else {
113                        ourLog.error("Did not successfully remove lock entry. [uuid={}]", theUUID);
114                }
115        }
116
117        public MigrationResult migrate() {
118                ourLog.info("Loaded {} migration tasks", myTaskList.size());
119                MigrationResult retval = new MigrationResult();
120
121                // Lock the migration table so only one server migrates the database at once
122                try (HapiMigrationLock ignored = new HapiMigrationLock(myHapiMigrationStorageSvc)) {
123                        MigrationTaskList newTaskList = myHapiMigrationStorageSvc.diff(myTaskList);
124                        ourLog.info("{} of these {} migration tasks are new.  Executing them now.", newTaskList.size(), myTaskList.size());
125
126                        try (DriverTypeEnum.ConnectionProperties connectionProperties = getDriverType().newConnectionProperties(getDataSource())) {
127
128                                newTaskList.forEach(next -> {
129
130                                        next.setDriverType(getDriverType());
131                                        next.setDryRun(isDryRun());
132                                        next.setNoColumnShrink(isNoColumnShrink());
133                                        next.setConnectionProperties(connectionProperties);
134
135                                        executeTask(next, retval);
136                                });
137                        }
138                } catch (Exception e) {
139                        ourLog.error("Migration failed", e);
140                        throw e;
141                }
142
143                ourLog.info(retval.summary());
144
145                if (isDryRun()) {
146                        StringBuilder statementBuilder = buildExecutedStatementsString(retval);
147                        ourLog.info("SQL that would be executed:\n\n***********************************\n{}***********************************", statementBuilder);
148                }
149
150                return retval;
151        }
152
153        private void executeTask(BaseTask theTask, MigrationResult theMigrationResult) {
154                StopWatch sw = new StopWatch();
155                try {
156                        if (isDryRun()) {
157                                ourLog.info("Dry run {} {}", theTask.getMigrationVersion(), theTask.getDescription());
158                        } else {
159                                ourLog.info("Executing {} {}", theTask.getMigrationVersion(), theTask.getDescription());
160                        }
161                        preExecute(theTask);
162                        theTask.execute();
163                        postExecute(theTask, sw, true);
164                        theMigrationResult.changes += theTask.getChangesCount();
165                        theMigrationResult.executedStatements.addAll(theTask.getExecutedStatements());
166                        theMigrationResult.succeededTasks.add(theTask);
167                } catch (SQLException | HapiMigrationException e) {
168                        theMigrationResult.failedTasks.add(theTask);
169                        postExecute(theTask, sw, false);
170                        String description = theTask.getDescription();
171                        if (isBlank(description)) {
172                                description = theTask.getClass().getSimpleName();
173                        }
174                        String prefix = "Failure executing task \"" + description + "\", aborting! Cause: ";
175                        throw new HapiMigrationException(Msg.code(47) + prefix + e, theMigrationResult, e);
176                }
177        }
178
179        private void preExecute(BaseTask theTask) {
180                myCallbacks.forEach(action -> action.preExecution(theTask));
181
182        }
183
184        private void postExecute(BaseTask theNext, StopWatch theStopWatch, boolean theSuccess) {
185                myHapiMigrationStorageSvc.saveTask(theNext, Math.toIntExact(theStopWatch.getMillis()), theSuccess);
186        }
187
188        public void addTasks(Iterable<BaseTask> theMigrationTasks) {
189                if (HapiSystemProperties.isUnitTestModeEnabled()) {
190                        // Tests only need to initialize the schemas. No need to run all the migrations for every test.
191                        for (BaseTask task : theMigrationTasks) {
192                                if (task instanceof InitializeSchemaTask) {
193                                        addTask(task);
194                                }
195                        }
196                } else {
197                        myTaskList.append(theMigrationTasks);
198                }
199        }
200
201        public void addTask(BaseTask theTask) {
202                myTaskList.add(theTask);
203        }
204
205        public void setCallbacks(@Nonnull List<IHapiMigrationCallback> theCallbacks) {
206                Validate.notNull(theCallbacks);
207                myCallbacks = theCallbacks;
208        }
209
210        @VisibleForTesting
211        public void removeAllTasksForUnitTest() {
212                myTaskList.clear();
213        }
214
215        public void createMigrationTableIfRequired() {
216                myHapiMigrationStorageSvc.createMigrationTableIfRequired();
217        }
218}