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