| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.unomi.shell.migration.service; |
| |
| import com.fasterxml.jackson.core.type.TypeReference; |
| import com.fasterxml.jackson.databind.ObjectMapper; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.http.impl.client.CloseableHttpClient; |
| import org.apache.karaf.shell.api.console.Session; |
| import org.jline.reader.LineReader; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.*; |
| |
| import static org.apache.unomi.shell.migration.service.MigrationConfig.*; |
| import static org.apache.unomi.shell.migration.service.MigrationServiceImpl.MIGRATION_FS_ROOT_FOLDER; |
| |
| /** |
| * This class is instantiated for each migration process, it contains useful methods to handle the current migration lifecycle. |
| * |
| * This class allow for keeping track of the migration steps by persisting the steps and there state on the FileSystem, |
| * allowing for a migration to be able to restart from a failure in case it happens. |
| * |
| * This class allow also for logging the migration informations depending on the current context: |
| * - if executed in karaf shell using a karaf shell session, then the logging will be done in the shell console |
| * - if executed outside karaf shell using OSGI service direct, then the logging will be done using classical logger systems |
| * |
| * This class allow also to do a best effort on missing configuration information, by prompting questions in the karaf shell |
| * (not supported in case direct OSGI service usage) |
| */ |
| public class MigrationContext { |
| private static final Logger logger = LoggerFactory.getLogger(MigrationContext.class); |
| |
| private static final Path MIGRATION_FS_HISTORY_FILE = Paths.get(System.getProperty( "karaf.data" ), MIGRATION_FS_ROOT_FOLDER, "history.json"); |
| |
| private enum MigrationStepState { |
| STARTED, |
| COMPLETED |
| } |
| |
| protected MigrationContext(Session session, MigrationConfig migrationConfig) { |
| this.session = session; |
| this.migrationConfig = migrationConfig; |
| this.objectMapper = new ObjectMapper(); |
| } |
| |
| private final Session session; |
| private final MigrationConfig migrationConfig; |
| private final ObjectMapper objectMapper; |
| private CloseableHttpClient httpClient; |
| |
| private Map<String, MigrationStepState> history = new HashMap<>(); |
| private Map<String, String> userConfig = new HashMap<>(); |
| |
| /** |
| * Try to recover from a previous run |
| * I case we found an existing history we will ask if we want to recover or if we want to restart from the beginning |
| * (it is also configurable using the conf: recoverFromHistory) |
| */ |
| protected void tryRecoverFromHistory() throws IOException { |
| if (Files.exists(MIGRATION_FS_HISTORY_FILE)) { |
| if (getConfigBoolean(MIGRATION_HISTORY_RECOVER)) { |
| history = objectMapper.readValue(MIGRATION_FS_HISTORY_FILE.toFile(), new TypeReference<Map<String, MigrationStepState>>() {}); |
| } else { |
| cleanHistory(); |
| } |
| } |
| } |
| |
| /** |
| * this method allow for migration step execution: |
| * - in case the history already contains the given stepKey as COMPLETED, then the step won't be executed |
| * - in case the history doesn't contain the given stepKey, then the step will be executed |
| * Also this method is keeping track of the history by persisting it on the FileSystem. |
| * |
| * @param stepKey the key of the given step |
| * @param step the step to be performed |
| * @throws IOException |
| */ |
| public void performMigrationStep(String stepKey, MigrationStep step) throws Exception { |
| if (step == null || stepKey == null) { |
| throw new IllegalArgumentException("Migration step and/or key cannot be null"); |
| } |
| |
| // check if step already exists in history: |
| MigrationStepState stepState = history.get(stepKey); |
| if (stepState != MigrationStepState.COMPLETED) { |
| updateHistoryStep(stepKey, MigrationStepState.STARTED); |
| step.execute(); |
| updateHistoryStep(stepKey, MigrationStepState.COMPLETED); |
| } else { |
| printMessage("Migration step: " + stepKey + " already completed in previous run"); |
| } |
| } |
| |
| /** |
| * Clean history from FileSystem |
| * @throws IOException |
| */ |
| protected void cleanHistory() throws IOException { |
| Files.deleteIfExists(MIGRATION_FS_HISTORY_FILE); |
| } |
| |
| /** |
| * This will ask a question to the user and return the default answer if the user does not answer. |
| * |
| * @param msg String message to ask |
| * @param defaultAnswer String default answer |
| * @return the user's answer |
| * @throws IOException if there was a problem reading input from the console |
| */ |
| public String askUserWithDefaultAnswer(String msg, String defaultAnswer) throws IOException { |
| String answer = promptMessageToUser(msg); |
| if (StringUtils.isBlank(answer)) { |
| return defaultAnswer; |
| } |
| return answer; |
| } |
| |
| /** |
| * This method allow you to ask a question to the user. |
| * The answer is controlled before being return so the question will be ask until the user enter one the authorized answer |
| * |
| * @param msg String message to ask |
| * @param authorizedAnswer Array of possible answer, all answer must be in lower case |
| * @return the user answer |
| * @throws IOException if there was an error retrieving an answer from the user on the console |
| */ |
| public String askUserWithAuthorizedAnswer(String msg, List<String> authorizedAnswer) throws IOException { |
| String answer; |
| do { |
| answer = promptMessageToUser(msg); |
| } while (!authorizedAnswer.contains(answer.toLowerCase())); |
| return answer; |
| } |
| |
| /** |
| * This method allow you to prompt a message to the user. |
| * No control is done on the answer provided by the user. |
| * |
| * @param msg String message to prompt |
| * @return the user answer |
| */ |
| public String promptMessageToUser(String msg) { |
| if (session == null) { |
| throw new IllegalStateException("Cannot prompt message: " + msg + " to user. " + |
| "(In case you are using the migration tool out of Karaf shell context, please check the migration configuration: org.apache.unomi.migration.cfg)"); |
| } |
| LineReader reader = (LineReader) session.get(".jline.reader"); |
| return reader.readLine(msg, null); |
| } |
| |
| /** |
| * Print a message in the console. |
| * @param msg the message to print out with a newline |
| */ |
| public void printMessage(String msg) { |
| if (session == null) { |
| logger.info(msg); |
| } else { |
| PrintStream writer = session.getConsole(); |
| writer.println(msg); |
| } |
| } |
| |
| /** |
| * Print an exception along with a message in the console. |
| * @param msg the message to print out with a newline |
| * @param t the exception to dump in the shell console after the message |
| */ |
| public void printException(String msg, Throwable t) { |
| if (session == null) { |
| logger.error(msg, t); |
| } else { |
| PrintStream writer = session.getConsole(); |
| writer.println(msg); |
| t.printStackTrace(writer); |
| } |
| } |
| |
| /** |
| * Get config for property name, in case the property doesn't exist on file system config file |
| * Best effort will be made to prompt question in karaf shell to get the needed information |
| * |
| * @param name the name of the property |
| * @return the value of the property |
| * @throws IOException |
| */ |
| public String getConfigString(String name) throws IOException { |
| // special handling for esAddress that need to be built |
| if (CONFIG_ES_ADDRESS.equals(name)) { |
| String esAddresses = getConfigString(CONFIG_ES_ADDRESSES); |
| boolean sslEnabled = getConfigBoolean(CONFIG_ES_SSL_ENABLED); |
| return (sslEnabled ? "https://" : "http://") + esAddresses.split(",")[0].trim(); |
| } |
| |
| if (migrationConfig.getConfig().containsKey(name)) { |
| return migrationConfig.getConfig().get(name); |
| } |
| if (userConfig.containsKey(name)) { |
| return userConfig.get(name); |
| } |
| if (configProperties.containsKey(name)) { |
| MigrationConfigProperty migrateConfigProperty = configProperties.get(name); |
| String answer = askUserWithDefaultAnswer(migrateConfigProperty.getDescription(), migrateConfigProperty.getDefaultValue()); |
| userConfig.put(name, answer); |
| return answer; |
| } |
| return null; |
| } |
| |
| /** |
| * Get config for property name, in case the property doesn't exist on file system config file |
| * Best effort will be made to prompt question in karaf shell to get the needed information |
| * |
| * @param name the name of the property |
| * @return the value of the property |
| * @throws IOException |
| */ |
| public boolean getConfigBoolean(String name) throws IOException { |
| if (migrationConfig.getConfig().containsKey(name)) { |
| return Boolean.parseBoolean(migrationConfig.getConfig().get(name)); |
| } |
| if (userConfig.containsKey(name)) { |
| return Boolean.parseBoolean(userConfig.get(name)); |
| } |
| if (configProperties.containsKey(name)) { |
| MigrationConfigProperty migrateConfigProperty = configProperties.get(name); |
| boolean answer = askUserWithAuthorizedAnswer(migrateConfigProperty.getDescription(), Arrays.asList("yes", "no")).equalsIgnoreCase("yes"); |
| userConfig.put(name, answer ? "true" : "false"); |
| return answer; |
| } |
| return false; |
| } |
| |
| /** |
| * This HTTP client is configured to be used for ElasticSearch requests to be able to perform migrations requests. |
| * @return the http client. |
| */ |
| public CloseableHttpClient getHttpClient() { |
| return httpClient; |
| } |
| |
| |
| private void updateHistoryStep(String stepKey, MigrationStepState stepState) throws IOException { |
| printMessage("Migration step: " + stepKey + " reach: " + stepState); |
| history.put(stepKey, stepState); |
| objectMapper.writeValue(MIGRATION_FS_HISTORY_FILE.toFile(), history); |
| } |
| |
| protected void setHttpClient(CloseableHttpClient httpClient) { |
| this.httpClient = httpClient; |
| } |
| |
| /** |
| * A simple migration step to be performed |
| */ |
| public interface MigrationStep { |
| /** |
| * Do you migration a safe and unitary way, so that in case this step fail it can be re-executed safely |
| */ |
| void execute() throws Exception; |
| } |
| } |