/* * eXist Open Source Native XML Database * Copyright (C) 2001-2011 The eXist Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * $Id$ */ package org.exist.backup; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutorService; import org.exist.util.SystemExitCodes; import org.xml.sax.SAXException; import org.xmldb.api.DatabaseManager; import org.xmldb.api.base.Collection; import org.xmldb.api.base.Database; import org.xmldb.api.base.XMLDBException; import org.exist.util.ConfigurationHelper; import org.exist.xmldb.DatabaseInstanceManager; import org.exist.xmldb.XmldbURI; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.util.Properties; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.prefs.Preferences; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.xml.parsers.ParserConfigurationException; import org.exist.backup.restore.listener.ConsoleRestoreListener; import org.exist.backup.restore.listener.GuiRestoreListener; import org.exist.backup.restore.listener.RestoreListener; import org.exist.client.ClientFrame; import se.softhouse.jargo.*; import static org.exist.util.ArgumentUtil.getBool; import static org.exist.util.ArgumentUtil.getOpt; import static se.softhouse.jargo.Arguments.*; /** * Main.java * * @author Wolfgang Meier */ public class Main { private static final String USER_PROP = "user"; private static final String PASSWORD_PROP = "password"; private static final String URI_PROP = "uri"; private static final String CONFIGURATION_PROP = "configuration"; private static final String DRIVER_PROP = "driver"; private static final String CREATE_DATABASE_PROP = "create-database"; private static final String BACKUP_DIR_PROP = "backup-dir"; private static final String DEFAULT_USER = "admin"; private static final String DEFAULT_PASSWORD = ""; private static final String DEFAULT_URI = "xmldb:exist://"; private static final String DEFAULT_DRIVER = "org.exist.xmldb.DatabaseImpl"; private static final String DEFAULT_BACKUP_DIR = "backup"; /* general arguments */ private static final Argument<?> helpArg = helpArgument("-h", "--help"); private static final Argument<Boolean> guiArg = optionArgument("-U", "--gui") .description("start in GUI mode") .defaultValue(false) .build(); private static final Argument<Boolean> quietArg = optionArgument("-q", "--quiet") .description("be quiet. Just print errors.") .defaultValue(false) .build(); private static final Argument<Map<String, String>> optionArg = stringArgument("-o", "--option") .description("specify extra options: property=value. For available properties see client.properties.") .asKeyValuesWithKeyParser(StringParsers.stringParser()) .build(); /* user/pass arguments */ private static final Argument<String> userArg = stringArgument("-u", "--user") .description("set user.") .defaultValue(DEFAULT_USER) .build(); private static final Argument<String> passwordArg = stringArgument("-p", "--password") .description("set the password for connecting to the database.") .build(); private static final Argument<String> dbaPasswordArg = stringArgument("-P", "--dba-password") .description("if the backup specifies a different password for the admin user, use this option to specify the new password. Otherwise you will get a permission denied") .build(); /* backup arguments */ private static final Argument<String> backupCollectionArg = stringArgument("-b", "--backup") .description("backup the specified collection.") .build(); private static final Argument<File> backupOutputDirArg = fileArgument("-d", "--dir") .description("specify the directory to use for backups.") .build(); /* restore arguments */ private static final Argument<File> restoreArg = fileArgument("-r", "--restore") .description("read the specified __contents__.xml file and restore the resources described there.") .build(); private static final Argument<Boolean> rebuildExpathRepoArg = optionArgument("-R", "--rebuild") .description("rebuild the EXpath app repository after restore.") .defaultValue(false) .build(); private static Properties loadProperties() { // read properties final Path propFile = ConfigurationHelper.lookup("backup.properties"); final Properties properties = new Properties(); try { if (Files.isReadable(propFile)) { try(final InputStream pin = Files.newInputStream(propFile)) { properties.load(pin); } } else { try(final InputStream pin = Main.class.getResourceAsStream("backup.properties")) { properties.load(pin); } } } catch (final IOException e) { System.err.println("WARN - Unable to load properties from: " + propFile.toAbsolutePath().toString()); } return properties; } /** * Constructor for Main. * * @param arguments parsed command line arguments */ public static void process(final ParsedArguments arguments) { final Properties properties = loadProperties(); final Preferences preferences = Preferences.userNodeForPackage(Main.class); final boolean guiMode = getBool(arguments, guiArg); final boolean quiet = getBool(arguments, quietArg); Optional.ofNullable(arguments.get(optionArg)).ifPresent(options -> options.forEach(properties::setProperty)); properties.setProperty(USER_PROP, arguments.get(userArg)); final String optionPass = arguments.get(passwordArg); properties.setProperty(PASSWORD_PROP, optionPass); final Optional<String> optionDbaPass = getOpt(arguments, dbaPasswordArg); final Optional<String> backupCollection = getOpt(arguments, backupCollectionArg); getOpt(arguments, backupOutputDirArg).ifPresent(backupOutputDir -> properties.setProperty(BACKUP_DIR_PROP, backupOutputDir.getAbsolutePath())); final Optional<Path> restorePath = getOpt(arguments, restoreArg).map(File::toPath); final boolean rebuildRepo = getBool(arguments, rebuildExpathRepoArg); // initialize driver Database database; try { final Class<?> cl = Class.forName(properties.getProperty(DRIVER_PROP, DEFAULT_DRIVER)); database = (Database) cl.newInstance(); database.setProperty(CREATE_DATABASE_PROP, "true"); if (properties.containsKey(CONFIGURATION_PROP)) { database.setProperty(CONFIGURATION_PROP, properties.getProperty(CONFIGURATION_PROP)); } DatabaseManager.registerDatabase(database); } catch (final ClassNotFoundException | InstantiationException | XMLDBException | IllegalAccessException e) { reportError(e); return; } // process if (backupCollection.isPresent()) { String collection = backupCollection.get(); if (collection.isEmpty()) { if (guiMode) { final CreateBackupDialog dialog = new CreateBackupDialog(properties.getProperty(URI_PROP, DEFAULT_URI), properties.getProperty(USER_PROP, DEFAULT_USER), properties.getProperty(PASSWORD_PROP, DEFAULT_PASSWORD), Paths.get(preferences.get("directory.backup", System.getProperty("user.dir")))); if (JOptionPane.showOptionDialog(null, dialog, "Create Backup", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null) == JOptionPane.YES_OPTION) { collection = dialog.getCollection(); properties.setProperty(BACKUP_DIR_PROP, dialog.getBackupTarget()); } } else { collection = XmldbURI.ROOT_COLLECTION; } } if (!collection.isEmpty()) { try { final Backup backup = new Backup( properties.getProperty(USER_PROP, DEFAULT_USER), properties.getProperty(PASSWORD_PROP, DEFAULT_PASSWORD), Paths.get(properties.getProperty(BACKUP_DIR_PROP, DEFAULT_BACKUP_DIR)), XmldbURI.xmldbUriFor(properties.getProperty(URI_PROP, DEFAULT_URI) + collection), properties ); backup.backup(guiMode, null); } catch (final Exception e) { reportError(e); } } } if (restorePath.isPresent()) { Path path = restorePath.get(); if (!Files.exists(path) && guiMode) { final JFileChooser chooser = new JFileChooser(); chooser.setMultiSelectionEnabled(false); chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); if (chooser.showDialog(null, "Select backup file for restore") == JFileChooser.APPROVE_OPTION) { path = chooser.getSelectedFile().toPath(); } } if (Files.exists(path)) { final String username = properties.getProperty(USER_PROP, DEFAULT_USER); final String uri = properties.getProperty(URI_PROP, DEFAULT_URI); try { if (guiMode) { restoreWithGui(username, optionPass, optionDbaPass, path, uri); } else { restoreWithoutGui(username, optionPass, optionDbaPass, path, uri, rebuildRepo, quiet); } } catch (final Exception e) { reportError(e); } } } try { String uri = properties.getProperty(URI_PROP, XmldbURI.EMBEDDED_SERVER_URI_PREFIX); if (!(uri.contains(XmldbURI.ROOT_COLLECTION) || uri.endsWith(XmldbURI.ROOT_COLLECTION))) { uri += XmldbURI.ROOT_COLLECTION; } final Collection root = DatabaseManager.getCollection(uri, properties.getProperty(USER_PROP, DEFAULT_USER), optionDbaPass.orElse(optionPass)); shutdown(root); } catch (final Exception e) { reportError(e); } System.exit(SystemExitCodes.OK_EXIT_CODE); } private static void restoreWithoutGui(final String username, final String password, final Optional<String> dbaPassword, final Path f, final String uri, final boolean rebuildRepo, boolean quiet) { final RestoreListener listener = new ConsoleRestoreListener(quiet); final Restore restore = new Restore(); try { restore.restore(listener, username, password, dbaPassword.orElse(null), f, uri); } catch (final IOException | URISyntaxException | ParserConfigurationException | XMLDBException | SAXException ioe) { listener.error(ioe.getMessage()); } if (listener.hasProblems()) { System.err.println(listener.warningsAndErrorsAsString()); } if (rebuildRepo) { System.out.println("Rebuilding application repository ..."); System.out.println("URI: " + uri); try { String rootURI = uri; if (!(rootURI.contains(XmldbURI.ROOT_COLLECTION) || rootURI.endsWith(XmldbURI.ROOT_COLLECTION))) { rootURI += XmldbURI.ROOT_COLLECTION; } final Collection root = DatabaseManager.getCollection(rootURI, username, dbaPassword.orElse(password)); if (root != null) { ClientFrame.repairRepository(root); System.out.println("Application repository rebuilt successfully."); } else { System.err.println("Failed to retrieve root collection: " + uri); } } catch (XMLDBException e) { reportError(e); System.err.println("Rebuilding application repository failed!"); } } else { System.out.println("\nIf you restored collections inside /db/apps, you may want\n" + "to rebuild the application repository. To do so, run the following query\n" + "as admin:\n\n" + "import module namespace repair=\"http://exist-db.org/xquery/repo/repair\"\n" + "at \"resource:org/exist/xquery/modules/expathrepo/repair.xql\";\n" + "repair:clean-all(),\n" + "repair:repair()\n"); } } private static void restoreWithGui(final String username, final String password, final Optional<String> dbaPassword, final Path f, final String uri) { final GuiRestoreListener listener = new GuiRestoreListener(); final Callable<Void> callable = new Callable<Void>() { @Override public Void call() throws Exception { final Restore restore = new Restore(); try { restore.restore(listener, username, password, dbaPassword.orElse(null), f, uri); listener.hideDialog(); if (JOptionPane.showConfirmDialog(null, "Would you like to rebuild the application repository?\nThis is only necessary if application packages were restored.", "Rebuild App Repository?", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { System.out.println("Rebuilding application repository ..."); try { String rootURI = uri; if (!(rootURI.contains(XmldbURI.ROOT_COLLECTION) || rootURI.endsWith(XmldbURI.ROOT_COLLECTION))) { rootURI += XmldbURI.ROOT_COLLECTION; } final Collection root = DatabaseManager.getCollection(rootURI, username, dbaPassword.orElse(password)); ClientFrame.repairRepository(root); System.out.println("Application repository rebuilt successfully."); } catch (XMLDBException e) { reportError(e); System.err.println("Rebuilding application repository failed!"); } } } catch (final Exception e) { ClientFrame.showErrorMessage(e.getMessage(), null); //$NON-NLS-1$ } finally { if (listener.hasProblems()) { ClientFrame.showErrorMessage(listener.warningsAndErrorsAsString(), null); } } return null; } }; final ExecutorService executor = Executors.newSingleThreadExecutor(); final Future<Void> future = executor.submit(callable); while (!future.isDone() && !future.isCancelled()) { try { future.get(100, TimeUnit.MILLISECONDS); } catch (final InterruptedException ie) { } catch (final ExecutionException ee) { break; } catch (final TimeoutException te) { } } } private static void reportError(final Throwable e) { e.printStackTrace(); if (e.getCause() != null) { System.err.println("caused by "); e.getCause().printStackTrace(); } System.exit(SystemExitCodes.CATCH_ALL_GENERAL_ERROR_EXIT_CODE); } private static void shutdown(final Collection root) { try { final DatabaseInstanceManager mgr = (DatabaseInstanceManager) root.getService("DatabaseInstanceManager", "1.0"); if (mgr == null) { System.err.println("service is not available"); } else if (mgr.isLocalInstance()) { System.out.println("shutting down database..."); mgr.shutdown(); } } catch (final XMLDBException e) { System.err.println("database shutdown failed: "); e.printStackTrace(); } } public static void main(final String[] args) { try { final ParsedArguments arguments = CommandLineParser .withArguments(userArg, passwordArg, dbaPasswordArg) .andArguments(backupCollectionArg, backupOutputDirArg) .andArguments(restoreArg, rebuildExpathRepoArg) .andArguments(helpArg, guiArg, quietArg, optionArg) .parse(args); process(arguments); } catch(final ArgumentException e) { System.out.println(e.getMessageAndUsage()); System.exit(SystemExitCodes.INVALID_ARGUMENT_EXIT_CODE); } catch (final Throwable e) { e.printStackTrace(); System.exit(SystemExitCodes.CATCH_ALL_GENERAL_ERROR_EXIT_CODE); } } }