/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.autosave; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Properties; import java.util.logging.Level; import javax.swing.SwingUtilities; import com.rapidminer.FileProcessLocation; import com.rapidminer.Process; import com.rapidminer.ProcessLocation; import com.rapidminer.RapidMiner; import com.rapidminer.RepositoryProcessLocation; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.processeditor.ExtendedProcessEditor; import com.rapidminer.gui.tools.UpdateQueue; import com.rapidminer.operator.Operator; import com.rapidminer.repository.MalformedRepositoryLocationException; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.tools.FileSystemService; import com.rapidminer.tools.LogService; import com.rapidminer.tools.XMLException; /** * This class handles the automatic saving (and potential recovery after a Studio crash) of the * current process. * * @author Venkatesh Umaashankar, Marco Boeck * */ public class AutoSave { private static final String LOCATION_TYPE_REPOSITORY = "repository_object"; private static final String LOCATION_TYPE_FILE = "file"; private static final String LOCATION_TYPE_NONE = "none"; private static final String PROPERTY_PROCESS_PATH = "autosave.process.path"; private static final String PROPERTY_PROCESS_TYPE = "autosave.process.type"; private Properties autoSaveProperties; private Path autoSavedProcessPropertiesPath; private Path autoSavedProcessPath; private UpdateQueue autoSaveQueue; private boolean autoSaveEnabled; private boolean isRecoveryProcessPresent; /** * Initializes auto save functionality for current Studio session and also checks if an auto * save exists. */ public void init() { // already initialized if (autoSaveEnabled) { return; } String rapidMinerDir = FileSystemService.getUserRapidMinerDir().getAbsolutePath(); Path autosaveDir = null; try { autosaveDir = Paths.get(rapidMinerDir, "autosave"); if (!Files.exists(autosaveDir)) { Files.createDirectory(autosaveDir); } autoSavedProcessPropertiesPath = autosaveDir.resolve("autosaved_process.properties"); autoSavedProcessPath = autosaveDir.resolve("autosaved_process.xml"); autoSaveQueue = new UpdateQueue("autosave-queue"); autoSaveQueue.start(); // all good, enable auto save this.autoSaveEnabled = true; } catch (IOException e1) { LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.autosave.AutoSave.dir_creation_failed"); this.autoSaveEnabled = false; // we can't recover if we fail here, therefore return return; } autoSaveProperties = new Properties(); // if properties file exists we have an auto saved process if (Files.exists(autoSavedProcessPropertiesPath)) { try (InputStream inputStream = new FileInputStream(autoSavedProcessPropertiesPath.toFile()); Reader autoSavePropertiesReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { if (Files.exists(autoSavedProcessPropertiesPath)) { autoSaveProperties.load(autoSavePropertiesReader); if (this.autoSaveEnabled) { // ask if user wants to recover his previous process isRecoveryProcessPresent = true; } } } catch (IOException e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.autosave.AutoSave.access_failed", e); this.autoSaveEnabled = false; // no need to add listeners here, we cannot save anyway return; } } // add listener which auto saves the process on every change RapidMinerGUI.getMainFrame().addExtendedProcessEditor(new ExtendedProcessEditor() { @Override public void setSelection(List<Operator> selection) { // do nothing } @Override public void processUpdated(Process process) { saveProcess(process); } @Override public void processChanged(Process process) { saveProcess(process); } @Override public void processViewChanged(Process process) { // do nothing } }); // called when RapidMiner has shutdown gracefully, in that case we can delete the autosave RapidMiner.addShutdownHook(new Runnable() { @Override public void run() { AutoSave.this.onShutdown(); } }); } /** * Returns whether there is an autosaved process, which can be used for recovery. Should be * called after {@link #init()}. * * @return {@code true} if there is an autosaved process for recovery */ public boolean isRecoveryProcessPresent() { return isRecoveryProcessPresent; } /** * Returns the path of the autosaved process if the process has a path. Should be called after * {@link #init()}. * * @return the path of the autosaved process or {@code null} if no path was associated to the * process */ public String getAutosavedPath() { String processPath = autoSaveProperties.getProperty(PROPERTY_PROCESS_PATH); return LOCATION_TYPE_NONE.equals(processPath) ? null : processPath; } /** * Recovers the autosaved process, if present. */ public void recoverAutosavedProcess() { if (!isRecoveryProcessPresent()) { return; } String processType = autoSaveProperties.getProperty(PROPERTY_PROCESS_TYPE); String processPath = autoSaveProperties.getProperty(PROPERTY_PROCESS_PATH); ProcessLocation autoSaveProcessLocation = new FileProcessLocation(autoSavedProcessPath.toFile()); ProcessLocation actualProcessLocation = null; if (processType.equals(LOCATION_TYPE_REPOSITORY)) { try { actualProcessLocation = new RepositoryProcessLocation(new RepositoryLocation(processPath)); } catch (MalformedRepositoryLocationException e) { // in that case location just stays null } } else if (processType.equals(LOCATION_TYPE_FILE)) { actualProcessLocation = new FileProcessLocation(Paths.get(processPath).toFile()); } // try restoring the process Process process = null; try { process = autoSaveProcessLocation.load(null); } catch (IOException | XMLException e) { // failed to recover process but can continue to auto save new ones LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.autosave.AutoSave.load_process_failed", e); } // if process successfully restored, open it in Studio if (process != null) { process.setProcessLocation(actualProcessLocation); if (actualProcessLocation != null) { RapidMinerGUI.getMainFrame().setOpenedProcess(process); } else { RapidMinerGUI.getMainFrame().setProcess(process, true); } process.updateNotify(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { RapidMinerGUI.getMainFrame().SAVE_ACTION.setEnabled(true); } }); } } /** * Save the given process as an auto save. * * @param process * the process to save */ private void saveProcess(final Process process) { // no need to do anything if auto save is disabled if (!autoSaveEnabled) { return; } // store saving in update queue to avoid multiple saves in a row this.autoSaveQueue.execute(new Runnable() { @Override public void run() { ProcessLocation processLocation = process.getProcessLocation(); if (processLocation != null) { if (processLocation instanceof FileProcessLocation) { autoSaveProperties.put(PROPERTY_PROCESS_PATH, ((FileProcessLocation) processLocation).getFile().getAbsolutePath()); autoSaveProperties.put(PROPERTY_PROCESS_TYPE, LOCATION_TYPE_FILE); } else if (processLocation instanceof RepositoryProcessLocation) { autoSaveProperties.put(PROPERTY_PROCESS_PATH, ((RepositoryProcessLocation) processLocation).getRepositoryLocation().getAbsoluteLocation()); autoSaveProperties.put(PROPERTY_PROCESS_TYPE, LOCATION_TYPE_REPOSITORY); } } else { // process is not saved yet autoSaveProperties.put(PROPERTY_PROCESS_PATH, LOCATION_TYPE_NONE); autoSaveProperties.put(PROPERTY_PROCESS_TYPE, LOCATION_TYPE_NONE); } String processXML = process.getRootOperator().getXML(false); try (OutputStreamWriter infoWriter = new OutputStreamWriter( new FileOutputStream(autoSavedProcessPropertiesPath.toFile()), StandardCharsets.UTF_8); OutputStreamWriter processWriter = new OutputStreamWriter( new FileOutputStream(autoSavedProcessPath.toFile()), StandardCharsets.UTF_8)) { autoSaveProperties.store(infoWriter, null); processWriter.write(processXML); processWriter.flush(); // process has been overwritten, we do not longer provide the recovery process isRecoveryProcessPresent = false; } catch (IOException e) { LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.autosave.AutoSave.dir_creation_failed", e); AutoSave.this.autoSaveEnabled = false; } } }); } /** * Indicate a graceful shutdown where deletes the auto save because it is not needed. */ private void onShutdown() { if (autoSaveEnabled) { try { Files.deleteIfExists(autoSavedProcessPropertiesPath); Files.deleteIfExists(autoSavedProcessPath); } catch (IOException e) { LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.autosave.AutoSave.deletion_failed", e); } } } }