/* * JBoss, Home of Professional Open Source * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors * as indicated by the @authors tag. All rights reserved. * See the copyright.txt in the distribution for a * full listing of individual contributors. * * This copyrighted material is made available to anyone wishing to use, * modify, copy, or redistribute it subject to the terms and conditions * of the GNU Lesser General Public License, v. 2.1. * This program is distributed in the hope that it will be useful, but WITHOUT A * 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, * v.2.1 along with this distribution; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ package org.jboss.as.controller.persistence; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import org.jboss.as.controller.logging.ControllerLogger; import org.jboss.as.controller.persistence.ConfigurationPersister.SnapshotInfo; import org.wildfly.security.manager.WildFlySecurityManager; /** * Encapsulates the configuration file and manages its history * * @author <a href="kabir.khan@jboss.com">Kabir Khan</a> * @author Brian Stansberry */ public class ConfigurationFile { /** * Policy controlling how to deal with the configuration file */ public enum InteractionPolicy { /** The typical case; require that the specified file exist and allow updates to it */ STANDARD(true, false, false, false), /** Delete the existing file if it exists and create a new empty file */ DISCARD(false, false, true, false), /** Fail if there is an existing file and it is non-empty; otherwise create a new empty file */ NEW(false, true, false, false), /** Require that the specified file exist, but do not update it */ READ_ONLY(true, false, false, true); private final boolean requireExisting; private final boolean rejectExisting; private final boolean removeExisting; private final boolean readOnly; private InteractionPolicy(boolean requireExisting, boolean rejectExisting, boolean removeExisting, boolean readOnly) { this.requireExisting = requireExisting; this.rejectExisting = rejectExisting; this.removeExisting = removeExisting; this.readOnly = readOnly; } public boolean isReadOnly() { return readOnly; } private boolean isRequireExisting() { return requireExisting; } private boolean isRejectExisting() { return rejectExisting; } private boolean isRemoveExisting() { return removeExisting; } } private static final String LAST = "last"; private static final String INITIAL = "initial"; private static final String BOOT = "boot"; private static final String LAST_SUFFIX = LAST + ".xml"; private static final String INITIAL_SUFFIX = INITIAL + ".xml"; private static final String ORIGINAL_SUFFIX = BOOT + ".xml"; private static final int CURRENT_HISTORY_LENGTH = 100; private static final int HISTORY_DAYS = 30; private static final String CURRENT_HISTORY_LENGTH_PROPERTY = "jboss.config.current-history-length"; private static final String HISTORY_DAYS_PROPERTY = "jboss.config.history-days"; private static final String TIMESTAMP_STRING = "\\d\\d\\d\\d\\d\\d\\d\\d-\\d\\d\\d\\d\\d\\d\\d\\d\\d"; private static final Pattern TIMESTAMP_PATTERN = Pattern.compile(TIMESTAMP_STRING); private static final String TIMESTAMP_FORMAT = "yyyyMMdd-HHmmssSSS"; private static final Pattern VERSION_PATTERN = Pattern.compile("v\\d+"); private static final Pattern FILE_WITH_VERSION_PATTERN = Pattern.compile("\\S*\\.v\\d+\\.xml"); private static final Pattern SNAPSHOT_XML = Pattern.compile(TIMESTAMP_STRING + "\\S*\\.xml"); private final AtomicInteger sequence = new AtomicInteger(); private final AtomicBoolean doneBootup = new AtomicBoolean(); private final File configurationDir; private final String rawFileName; private volatile String bootFileName; // File from which boot operations should be parsed; null if currently undetermined private volatile File bootFile; /* Whether the next determination of the bootFile should use the .last file in history instead of the {@link #mainFile}. Only relevant with {@link InteractionPolicy#READ_ONLY}. If true and used with a non-null newReloadBootFileName, the newReloadBootFileName will take precedence */ private volatile boolean reloadUsingLast; // Whether {@link #bootFile has been reset from its first value private volatile boolean bootFileReset; // A new boot file specified during a reload process. This will take precedence over a true reloadUsingLast value. private volatile String newReloadBootFileName; private final File mainFile; private final File historyRoot; private final File currentHistory; private final File snapshotsDirectory; // Policy governing how to interact with the physical file private final InteractionPolicy interactionPolicy; /* Backup copy of the most recent configuration, stored in the history dir. May be used as {@link #bootFile}; see {@link #reloadUsingLast} */ private volatile File lastFile; /** * Creates a new ConfigurationFile. * * @param configurationDir directory in which configuration files are stored. Cannot be {@code null} and must exist * and be a directory * @param rawName default name for configuration files of the type handled by this object. * Cannot be {@code null} or an empty string * @param name user provided name of the configuration file to use * @param persistOriginal {@code true} if configuration modifications should be persisted back to the main * configuration file; {@code false} if they should only be persisted * to the configuration history directory */ public ConfigurationFile(final File configurationDir, final String rawName, final String name, final boolean persistOriginal) { this(configurationDir, rawName, name, persistOriginal ? InteractionPolicy.STANDARD : InteractionPolicy.READ_ONLY); } /** * Creates a new ConfigurationFile. * * @param configurationDir directory in which configuration files are stored. Cannot be {@code null} and must exist * and be a directory * @param rawName default name for configuration files of the type handled by this object. * Cannot be {@code null} or an empty string * @param name user provided name of the configuration file to use * @param interactionPolicy policy governing interaction with the configuration file */ public ConfigurationFile(final File configurationDir, final String rawName, final String name, final InteractionPolicy interactionPolicy) { if (!configurationDir.exists() || !configurationDir.isDirectory()) { throw ControllerLogger.ROOT_LOGGER.directoryNotFound(configurationDir.getAbsolutePath()); } assert rawName != null && rawName.length() > 0; this.rawFileName = rawName; this.bootFileName = name != null ? name : rawName; this.configurationDir = configurationDir; this.historyRoot = new File(configurationDir, rawName.replace('.', '_') + "_history"); this.currentHistory = new File(historyRoot, "current"); this.snapshotsDirectory = new File(historyRoot, "snapshot"); this.interactionPolicy = interactionPolicy == null ? InteractionPolicy.STANDARD : interactionPolicy; final File file = determineMainFile(rawName, name); try { this.mainFile = file.getCanonicalFile(); } catch (IOException ioe) { throw ControllerLogger.ROOT_LOGGER.canonicalMainFileNotFound(ioe, file); } } public boolean checkCanFindNewBootFile(final String bootFileName) { File file = determineBootFile(configurationDir, bootFileName); return file != null && file.exists(); } /** * Reset so the next call to {@link #getBootFile()} will re-determine the appropriate file to use for * parsing boot operations. If {@code reloadUsingLast} is {@code true}, while {@code newBootFileName} is not {@code null}, * {@code newBootFileName} will take precedence. If a {@code newBootFileName} is used, callers must call * {@link #checkCanFindNewBootFile(String)} first. * * @param reloadUsingLast {@code true} if the next call to {@link #getBootFile()} should use the last file from * the history. Only relevant if this object is not persisting changes * back to the original source file * @param newBootFileName the name of the new bootfile */ public synchronized void resetBootFile(boolean reloadUsingLast, String newBootFileName) { this.bootFile = null; this.bootFileReset = true; this.reloadUsingLast = reloadUsingLast; this.newReloadBootFileName = newBootFileName; } /** * Gets the file from which boot operations should be parsed. * @return the file. Will not be {@code null} */ public File getBootFile() { if (bootFile == null) { synchronized (this) { if (bootFile == null) { if (bootFileReset) { //Reset the done bootup and the sequence, so that the old file we are reloading from // overwrites the main file on successful boot, and history is reset as when booting new doneBootup.set(false); sequence.set(0); } // If it's a reload with no new boot file name and we're persisting our config, we boot from mainFile, // as that's where we persist if (bootFileReset && !interactionPolicy.isReadOnly() && newReloadBootFileName == null) { // we boot from mainFile bootFile = mainFile; } else { // It's either first boot, or a reload where we're not persisting our config or with a new boot file. // So we need to figure out which file we're meant to boot from String bootFileName = this.bootFileName; if (newReloadBootFileName != null) { //A non-null new boot file on reload takes precedence over the reloadUsingLast functionality //A new boot file was specified. Use that and reset the new name to null bootFileName = newReloadBootFileName; newReloadBootFileName = null; } else if (interactionPolicy.isReadOnly() && reloadUsingLast) { //If we were reloaded, and it is not a persistent configuration we want to use the last from the history bootFileName = LAST; } boolean usingRawFile = bootFileName.equals(rawFileName); if (usingRawFile) { bootFile = mainFile; } else { bootFile = determineBootFile(configurationDir, bootFileName); try { bootFile = bootFile.getCanonicalFile(); } catch (IOException ioe) { throw ControllerLogger.ROOT_LOGGER.canonicalBootFileNotFound(ioe, bootFile); } } if (!bootFile.exists()) { if (!usingRawFile) { // TODO there's no reason usingRawFile should be an exception, // but the test infrastructure stuff is built around an assumption // that ConfigurationFile doesn't fail if test files are not // in the normal spot if (bootFileReset || interactionPolicy.isRequireExisting()) { throw ControllerLogger.ROOT_LOGGER.fileNotFound(bootFile.getAbsolutePath()); } } // Create it for the NEW and DISCARD cases if (!bootFileReset && !interactionPolicy.isRequireExisting()) { createBootFile(bootFile); } } else if (!bootFileReset) { if (interactionPolicy.isRejectExisting() && bootFile.length() > 0) { throw ControllerLogger.ROOT_LOGGER.rejectEmptyConfig(bootFile.getAbsolutePath()); } else if (interactionPolicy.isRemoveExisting() && bootFile.length() > 0) { if (!bootFile.delete()) { throw ControllerLogger.ROOT_LOGGER.cannotDelete(bootFile.getAbsoluteFile()); } createBootFile(bootFile); } } // else after first boot we want the file to exist } } } } return bootFile; } public InteractionPolicy getInteractionPolicy() { return interactionPolicy; } /** * Given {@code name}, determine the intended main configuration file. Handles special cases, including * "last", "initial", "boot", "v1", and, if persistence to the original file is not supported, absolute paths. * * @param rawName default name for the main configuration file. Cannot be {@code null} * @param name user provided name of the main configuration, or {@code null} if not was provided */ private File determineMainFile(final String rawName, final String name) { assert rawName != null; String mainName = null; if (name == null) { // Just use the default mainName = rawName; } else if (name.equals(LAST) || name.equals(INITIAL) || name.equals(BOOT)) { // Search for a *single* file in the configuration dir with suffix == name.xml mainName = findMainFileFromBackupSuffix(historyRoot, name); } else if (VERSION_PATTERN.matcher(name).matches()) { // Search for a *single* file in the currentHistory dir with suffix == name.xml mainName = findMainFileFromBackupSuffix(currentHistory, name); } if (mainName == null) { // Search for a *single* file in the snapshots dir with prefix == name.xml mainName = findMainFileFromSnapshotPrefix(name); } if (mainName == null) { // Try the basic case, where name is the name final File directoryFile = new File(configurationDir, name); if (directoryFile.exists()) { mainName = stripPrefixSuffix(name); // TODO what if the stripped name doesn't exist? And why would there be a file like configuration/standalone.last.xml? } else if (interactionPolicy.isReadOnly()) { // We allow absolute paths in this case final File absoluteFile = new File(name); if (absoluteFile.exists()) { return absoluteFile; } } } if (mainName == null && !interactionPolicy.isRequireExisting()) { mainName = stripPrefixSuffix(name); } if (mainName != null) { return new File(configurationDir, new File(mainName).getName()); } throw ControllerLogger.ROOT_LOGGER.mainFileNotFound(name != null ? name : rawName, configurationDir); } /** * Finds a single file in {@code searchDir} whose name ends with "{@code .backupType.xml}" * and returns its name with {@code .backupType} removed. * * @param searchDir the directory to search * @param backupType the backup type; {@link #LAST}, {@link #BOOT}, {@link #INITIAL} or {@code v\d+} * @return the single file that meets the criteria. Will not return {@code null} * @throws IllegalStateException if no files meet the criteria or more than one does * @throws IllegalArgumentException if they file that meets the criteria's full name is "{@code backupType.xml}" */ private String findMainFileFromBackupSuffix(File searchDir, String backupType) { final String suffix = "." + backupType + ".xml"; File[] files = null; if (searchDir.exists() && searchDir.isDirectory()) { files = searchDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(suffix); } }); } if (files == null || files.length == 0) { throw ControllerLogger.ROOT_LOGGER.configurationFileNotFound(suffix, searchDir); } else if (files.length > 1) { throw ControllerLogger.ROOT_LOGGER.ambiguousConfigurationFiles(backupType, searchDir, suffix); } String matchName = files[0].getName(); if (matchName.equals(suffix)) { throw ControllerLogger.ROOT_LOGGER.configurationFileNameNotAllowed(backupType); } String prefix = matchName.substring(0, matchName.length() - suffix.length()); return prefix + ".xml"; } /** * Finds a single file in the snapshots directory whose name starts with {@code prefix} and * returns its name with the prefix removed. * * @param prefix the prefix * @return the single file that meets the criterion {@code null} if none do * @throws IllegalStateException if more than one file meets the criteria */ private String findMainFileFromSnapshotPrefix(final String prefix) { File[] files = null; if (snapshotsDirectory.exists() && snapshotsDirectory.isDirectory()) { files = snapshotsDirectory.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.startsWith(prefix); } }); } if (files == null || files.length == 0) { return null; } else if (files.length > 1) { throw ControllerLogger.ROOT_LOGGER.ambiguousConfigurationFiles(prefix, snapshotsDirectory, prefix); } String matchName = files[0].getName(); return matchName.substring(TIMESTAMP_FORMAT.length()); } private String stripPrefixSuffix(String name) { if (SNAPSHOT_XML.matcher(name).matches()) { name = name.substring(TIMESTAMP_FORMAT.length()); } if (FILE_WITH_VERSION_PATTERN.matcher(name).matches()) { int last = name.lastIndexOf('v'); name = name.substring(0, last) + "xml"; } else if (name.endsWith(LAST_SUFFIX)) { name = name.substring(0, name.length() - (LAST_SUFFIX).length()) + "xml"; } else if (name.endsWith(ORIGINAL_SUFFIX)) { name = name.substring(0, name.length() - (ORIGINAL_SUFFIX).length()) + "xml"; } else if (name.endsWith(INITIAL_SUFFIX)) { name = name.substring(0, name.length() - (INITIAL_SUFFIX).length()) + "xml"; } return name; } private File determineBootFile(final File configurationDir, final String name) { final File directoryFile = new File(configurationDir, name); File result; if (name.equals(LAST) || name.equals(INITIAL) || name.equals(BOOT)) { result = addSuffixToFile(new File(historyRoot, mainFile.getName()), name); } else if (VERSION_PATTERN.matcher(name).matches()) { result = getVersionedFile(mainFile, name); } else { result = findSnapshotWithPrefix(name, false); if (result == null) { if (directoryFile.exists()) { result = directoryFile; } else if (interactionPolicy.isReadOnly()) { File absoluteFile = new File(name); if (absoluteFile.exists()) { result = absoluteFile; } } } } if (result == null) { // We know directoryFile doesn't exist or the above logic would have set result to it. // But we use that as our last alternative. Let the caller object if non-existence is a problem result = directoryFile; } return result; } private static void createBootFile(File toCreate) { IOException cause = null; try { if (toCreate.createNewFile()) { return; } } catch (IOException e) { cause = e; } throw ControllerLogger.ROOT_LOGGER.cannotCreateEmptyConfig(toCreate.getAbsolutePath(), cause); } /** Gets the file to which modifications would be persisted, if this object is persisting changes outside the history directory */ File getMainFile() { return mainFile; } File getConfigurationDir(){ return configurationDir; } /** Notification that boot has completed successfully and the configuration history should be updated */ void successfulBoot() throws ConfigurationPersistenceException { synchronized (this) { if (doneBootup.get()) { return; } final File copySource; if (!interactionPolicy.isReadOnly()) { copySource = mainFile; } else { if ( FilePersistenceUtils.isParentFolderWritable(mainFile) ) { copySource = new File(mainFile.getParentFile(), mainFile.getName() + ".boot"); } else{ copySource = new File(configurationDir, mainFile.getName() + ".boot"); } FilePersistenceUtils.deleteFile(copySource); } try { if (!bootFile.equals(copySource)) { FilePersistenceUtils.copyFile(bootFile, copySource); } createHistoryDirectory(); final File historyBase = new File(historyRoot, mainFile.getName()); lastFile = addSuffixToFile(historyBase, LAST); final File boot = addSuffixToFile(historyBase, BOOT); final File initial = addSuffixToFile(historyBase, INITIAL); if (!initial.exists()) { FilePersistenceUtils.copyFile(copySource, initial); } FilePersistenceUtils.copyFile(copySource, lastFile); FilePersistenceUtils.copyFile(copySource, boot); } catch (IOException e) { throw ControllerLogger.ROOT_LOGGER.failedToCreateConfigurationBackup(e, bootFile); } finally { if (interactionPolicy.isReadOnly()) { //Delete the temporary file try { FilePersistenceUtils.deleteFile(copySource); } catch (Exception ignore) { } } } doneBootup.set(true); } } /** Backup the current version of the configuration to the versioned configuration history */ void backup() throws ConfigurationPersistenceException { if (!doneBootup.get()) { return; } try { if (!interactionPolicy.isReadOnly()) { //Move the main file to the versioned history moveFile(mainFile, getVersionedFile(mainFile)); } else { //Copy the Last file to the versioned history moveFile(lastFile, getVersionedFile(mainFile)); } int seq = sequence.get(); // delete unwanted backup files int currentHistoryLength = getInteger(CURRENT_HISTORY_LENGTH_PROPERTY, CURRENT_HISTORY_LENGTH, 0); if (seq > currentHistoryLength) { for (int k = seq - currentHistoryLength; k > 0; k--) { File delete = getVersionedFile(mainFile, k); if (! delete.exists()) { break; } delete.delete(); } } } catch (IOException e) { throw ControllerLogger.ROOT_LOGGER.failedToBackup(e, mainFile); } } /** * Commit the contents of the given temp file to either the main file, or, if we are not persisting * to the main file, to the .last file in the configuration history * @param temp temp file containing the latest configuration. Will not be {@code null} * @throws ConfigurationPersistenceException */ void commitTempFile(File temp) throws ConfigurationPersistenceException { if (!doneBootup.get()) { return; } if (!interactionPolicy.isReadOnly()) { FilePersistenceUtils.moveTempFileToMain(temp, mainFile); } else { FilePersistenceUtils.moveTempFileToMain(temp, lastFile); } } /** Notification that the configuration has been written, and its current content should be stored to the .last file */ void fileWritten() throws ConfigurationPersistenceException { if (!doneBootup.get() || interactionPolicy.isReadOnly()) { return; } try { FilePersistenceUtils.copyFile(mainFile, lastFile); } catch (IOException e) { throw ControllerLogger.ROOT_LOGGER.failedToBackup(e, mainFile); } } private void moveFile(final File file, final File backup) throws IOException { Files.move(file.toPath(), backup.toPath(), StandardCopyOption.REPLACE_EXISTING); } String snapshot() throws ConfigurationPersistenceException { String name = getTimeStamp(new Date()) + mainFile.getName(); File snapshot = new File(snapshotsDirectory, name); File source = interactionPolicy.isReadOnly() ? lastFile : mainFile; try { FilePersistenceUtils.copyFile(source, snapshot); } catch (IOException e) { throw ControllerLogger.ROOT_LOGGER.failedToTakeSnapshot(e, source, snapshot); } return snapshot.toString(); } SnapshotInfo listSnapshots() { return new BackupSnapshotInfo(); } void deleteSnapshot(final String prefix) { if (prefix.equals("all")) { if (snapshotsDirectory.exists() && snapshotsDirectory.isDirectory()) { for (String curr : snapshotsDirectory.list()) { new File(snapshotsDirectory, curr).delete(); } } } else { findSnapshotWithPrefix(prefix, true).delete(); } } private File findSnapshotWithPrefix(final String prefix, boolean errorIfNoFiles) { List<String> names = new ArrayList<String>(); if (snapshotsDirectory.exists() && snapshotsDirectory.isDirectory()) { for (String curr : snapshotsDirectory.list()) { if (curr.startsWith(prefix)) { names.add(curr); } } } if (names.size() == 0 && errorIfNoFiles) { throw ControllerLogger.ROOT_LOGGER.fileNotFoundWithPrefix(prefix, snapshotsDirectory.getAbsolutePath()); } if (names.size() > 1) { throw ControllerLogger.ROOT_LOGGER.ambiguousName(prefix, snapshotsDirectory.getAbsolutePath(), names); } return names.size() > 0 ? new File(snapshotsDirectory, names.get(0)) : null; } private void createHistoryDirectory() throws IOException { mkdir(this.historyRoot); mkdir(this.snapshotsDirectory); if (currentHistory.exists()) { if (!currentHistory.isDirectory()) { throw ControllerLogger.ROOT_LOGGER.notADirectory(currentHistory.getAbsolutePath()); } //Copy any existing history directory to a timestamped backup directory Date date = new Date(); File[] currentHistoryFiles = currentHistory.listFiles(); if (currentHistoryFiles != null && currentHistoryFiles.length > 0) { String backupName = getTimeStamp(date); File old = new File(historyRoot, backupName); if (!forcedMove(currentHistory.toPath(), old.toPath())) { if (old.exists()) { // AS7-5801. Unit tests sometimes fail on File.renameTo due to only having 100 ms // precision on the timestamps we use for dir names on some systems. So, if that happens, // we bump the timestamp once and try again before failing date = new Date(date.getTime() + 100); backupName = getTimeStamp(date); old = new File(historyRoot, backupName); if (!forcedMove(currentHistory.toPath(), old.toPath())) { ControllerLogger.ROOT_LOGGER.couldNotCreateHistoricalBackup(currentHistory.getAbsolutePath()); } } else { ControllerLogger.ROOT_LOGGER.couldNotCreateHistoricalBackup(currentHistory.getAbsolutePath()); } } } //Delete any old history directories int historyDays = getInteger(HISTORY_DAYS_PROPERTY, HISTORY_DAYS, 0); final String cutoffFileName = getTimeStamp(subtractDays(date, historyDays)); for (String name : historyRoot.list()) { if (name.length() == cutoffFileName.length() && TIMESTAMP_PATTERN.matcher(name).matches() && name.compareTo(cutoffFileName) < 0) { deleteRecursive(new File(historyRoot, name)); } } } //Create the history directory currentHistory.mkdir(); if (!currentHistory.exists()) { throw ControllerLogger.ROOT_LOGGER.cannotCreate(currentHistory.getAbsolutePath()); } } private int getInteger(final String name, final int defaultValue, final int minimalValue) { int retVal = getInteger(name, defaultValue); return (retVal < minimalValue) ? defaultValue : retVal; } private int getInteger(final String name, final int defaultValue) { final String val = WildFlySecurityManager.getPropertyPrivileged(name, null); try { return val == null ? defaultValue : Integer.parseInt(val); } catch (NumberFormatException ignored) { return defaultValue; } } private void deleteRecursive(final File file) { if (file.isDirectory()) { for (String name : file.list()) { deleteRecursive(new File(file, name)); } } if (!file.delete()) { throw ControllerLogger.ROOT_LOGGER.cannotDelete(file); } } private File getVersionedFile(final File file) { return getVersionedFile(file, sequence.incrementAndGet()); } private File getVersionedFile(final File file, int i) { return addSuffixToFile(new File(currentHistory, file.getName()), "v" + i); } private File getVersionedFile(final File file, String versionString) { return addSuffixToFile(new File(currentHistory, file.getName()), versionString); } private File addSuffixToFile(final File file, final String suffix) { Path path = file.toPath(); final String fileName = path.getFileName().toString(); int index = fileName.lastIndexOf('.'); if (index == -1) { return path.resolveSibling(fileName + '.' + suffix).toFile(); } StringBuilder sb = new StringBuilder(); sb.append(fileName.substring(0, index)); sb.append('.'); sb.append(suffix); sb.append(fileName.substring(index)); return path.resolveSibling(sb.toString()).toFile(); } private Date subtractDays(final Date date, final int days) { final Calendar calendar = new GregorianCalendar(); calendar.setTime(date); final int doy = calendar.get(Calendar.DAY_OF_YEAR); calendar.set(Calendar.DAY_OF_YEAR, doy - days); return calendar.getTime(); } private static String getTimeStamp(final Date date) { final SimpleDateFormat sfd = new SimpleDateFormat(TIMESTAMP_FORMAT); return sfd.format(date); } private File mkdir(final File dir) { if (!dir.exists()) { if (!dir.mkdir()) { throw ControllerLogger.ROOT_LOGGER.cannotCreate(historyRoot.getAbsolutePath()); } } else if (!dir.isDirectory()) { throw ControllerLogger.ROOT_LOGGER.notADirectory(dir.getAbsolutePath()); } return dir; } private static boolean forcedMove(Path from, Path to) { try { Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); return true; } catch (IOException e) { ControllerLogger.ROOT_LOGGER.cannotRename(e, from, to); return false; } } private class BackupSnapshotInfo implements SnapshotInfo { final ArrayList<String> names = new ArrayList<String>(); public BackupSnapshotInfo() { for (String name : snapshotsDirectory.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return SNAPSHOT_XML.matcher(name).matches(); } })) { names.add(name); } } @Override public String getSnapshotDirectory() { return snapshotsDirectory.getAbsolutePath(); } @Override public List<String> names() { return names; } } }