package org.jboss.as.patching.runner; import static org.jboss.as.patching.runner.PatchUtils.BACKUP_EXT; import static org.jboss.as.patching.runner.PatchUtils.JAR_EXT; import java.io.File; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import javax.xml.stream.XMLStreamException; import org.jboss.as.patching.Constants; import org.jboss.as.patching.DirectoryStructure; import org.jboss.as.patching.IoUtils; import org.jboss.as.patching.PatchInfo; import org.jboss.as.patching.PatchingException; import org.jboss.as.patching.installation.InstallationManager; import org.jboss.as.patching.installation.InstalledIdentity; import org.jboss.as.patching.installation.InstalledImage; import org.jboss.as.patching.installation.PatchableTarget; import org.jboss.as.patching.logging.PatchLogger; import org.jboss.as.patching.metadata.ContentItem; import org.jboss.as.patching.metadata.ContentModification; import org.jboss.as.patching.metadata.ContentType; import org.jboss.as.patching.metadata.LayerType; import org.jboss.as.patching.metadata.MiscContentItem; import org.jboss.as.patching.metadata.ModuleItem; import org.jboss.as.patching.metadata.Patch; import org.jboss.as.patching.metadata.PatchElement; import org.jboss.as.patching.metadata.PatchElementProvider; import org.jboss.as.patching.metadata.PatchImpl; import org.jboss.as.patching.metadata.PatchXml; import org.jboss.as.patching.metadata.RollbackPatch; import org.jboss.as.patching.metadata.impl.IdentityImpl; import org.jboss.as.patching.metadata.impl.PatchElementImpl; import org.jboss.as.patching.tool.ContentVerificationPolicy; import org.jboss.as.patching.tool.PatchingHistory; import org.jboss.as.patching.tool.PatchingResult; /** * @author Emanuel Muckenhuber */ class IdentityPatchContext implements PatchContentProvider { private final File miscBackup; private final File configBackup; private final File miscTargetRoot; private final PatchEntry identityEntry; private final InstalledImage installedImage; private final PatchContentProvider contentProvider; private final ContentVerificationPolicy contentPolicy; private final InstallationManager.InstallationModification modification; private final Map<String, PatchContentLoader> contentLoaders = new HashMap<String, PatchContentLoader>(); private final PatchingHistory history; // TODO initialize layers in the correct order private final Map<String, PatchEntry> layers = new LinkedHashMap<String, PatchEntry>(); private final Map<String, PatchEntry> addOns = new LinkedHashMap<String, PatchEntry>(); private PatchingTaskContext.Mode mode; private volatile State state = State.NEW; private boolean checkForGarbageOnRestart; // flag to trigger a cleanup on restart private static final AtomicReferenceFieldUpdater<IdentityPatchContext, State> stateUpdater = AtomicReferenceFieldUpdater.newUpdater(IdentityPatchContext.class, State.class, "state"); // The modules we need to invalidate private final List<File> moduleInvalidations = new ArrayList<File>(); private List<File> modulesToReenable = Collections.emptyList(); private List<File> modulesToDisable = Collections.emptyList(); private final Map<String, FailedFileRenaming> renames = new LinkedHashMap<String, FailedFileRenaming>(); static enum State { NEW, PREPARED, COMPLETED, INVALIDATE, ROLLBACK_ONLY, ; } IdentityPatchContext(final File backup, final PatchContentProvider contentProvider, final ContentVerificationPolicy contentPolicy, final InstallationManager.InstallationModification modification, final PatchingTaskContext.Mode mode, final InstalledImage installedImage) { this.miscTargetRoot = installedImage.getJbossHome(); this.mode = mode; this.contentProvider = contentProvider; this.contentPolicy = contentPolicy; this.modification = modification; this.installedImage = installedImage; this.history = PatchingHistory.Factory.getHistory(modification.getUnmodifiedInstallationState()); if (backup != null) { this.miscBackup = new File(backup, PatchContentLoader.MISC); this.configBackup = new File(backup, Constants.CONFIGURATION); } else { this.miscBackup = null; // This will trigger a failure when the root is actually needed this.configBackup = null; } this.identityEntry = new IdentityEntry(modification, null); } /** * Get the patch entry for the identity. * * @return the identity entry */ PatchEntry getIdentityEntry() { return identityEntry; } /** * Get a patch entry for either a layer or add-on. * * @param name the layer name * @param addOn whether the target is an add-on * @return the patch entry, {@code null} if it there is no such layer */ PatchEntry getEntry(final String name, boolean addOn) { return addOn ? addOns.get(name) : layers.get(name); } /** * Get all entries. * * @return the entries for all layers */ Collection<PatchEntry> getLayers() { return layers.values(); } /** * Get all add-ons. * * @return the entries for all add-ons */ Collection<PatchEntry> getAddOns() { return addOns.values(); } /** * Get the current modification. * * @return the modification */ InstallationManager.InstallationModification getModification() { return modification; } /** * Get the patch history. * * @return the history */ PatchingHistory getHistory() { return history; } /** * Get the current mode. * * @return the mode */ PatchingTaskContext.Mode getMode() { return mode; } /** * In case we cannot delete a directory create a marker to recheck whether we can garbage collect some not * referenced directories and files. * * @param file the directory */ protected void failedToCleanupDir(final File file) { checkForGarbageOnRestart = true; PatchLogger.ROOT_LOGGER.cannotDeleteFile(file.getAbsolutePath()); } protected void failedToRenameFile(final File file, final File target) { if (!renames.containsKey(file.getAbsolutePath())) { renames.put(file.getAbsolutePath(), new FailedFileRenaming(file, target, getIdentityEntry().applyPatchId)); PatchLogger.ROOT_LOGGER.cannotRenameFile(file.getAbsolutePath()); } } @Override public PatchContentLoader getLoader(final String patchId) { final PatchContentLoader loader = contentLoaders.get(patchId); if (loader != null) { return loader; } return contentProvider.getLoader(patchId); } @Override public void cleanup() { // If cleanup gets called before finalizePatch, something went wrong if (state != State.PREPARED) { undoChanges(); } } /** * Get the target entry for a given patch element. * * @param element the patch element * @return the patch entry * @throws PatchingException */ protected PatchEntry resolveForElement(final PatchElement element) throws PatchingException { assert state == State.NEW; final PatchElementProvider provider = element.getProvider(); final String layerName = provider.getName(); final LayerType layerType = provider.getLayerType(); final Map<String, PatchEntry> map; if (layerType == LayerType.Layer) { map = layers; } else { map = addOns; } PatchEntry entry = map.get(layerName); if (entry == null) { final InstallationManager.MutablePatchingTarget target = modification.resolve(layerName, layerType); if (target == null) { throw PatchLogger.ROOT_LOGGER.noSuchLayer(layerName); } entry = new PatchEntry(target, element); map.put(layerName, entry); } // Maintain the most recent element entry.updateElement(element); return entry; } /** * Finalize the patch. * * @param callback the finalize callback * @return the result * @throws Exception */ protected PatchingResult finalize(final FinalizeCallback callback) throws Exception { assert state == State.NEW; final Patch original = callback.getPatch(); final Patch.PatchType patchType = original.getIdentity().getPatchType(); final String patchId; if (patchType == Patch.PatchType.CUMULATIVE) { patchId = modification.getCumulativePatchID(); } else { patchId = original.getPatchId(); } try { // The processed patch, based on the recorded changes final Patch processedPatch = createProcessedPatch(original); // The rollback containing all the recorded rollback actions final RollbackPatch rollbackPatch = createRollbackPatch(patchId, patchType); callback.finishPatch(processedPatch, rollbackPatch, this); } catch (Exception e) { if (undoChanges()) { callback.operationCancelled(this); } throw e; } state = State.PREPARED; return new PatchingResult() { @Override public String getPatchId() { return original.getPatchId(); } @Override public PatchInfo getPatchInfo() { return new PatchInfo() { @Override public String getVersion() { return identityEntry.getResultingVersion(); } @Override public String getCumulativePatchID() { return identityEntry.delegate.getModifiedState().getCumulativePatchID(); } @Override public List<String> getPatchIDs() { return identityEntry.delegate.getModifiedState().getPatchIDs(); } }; } @Override public void commit() { if (state == State.PREPARED) { complete(modification, callback); } else { undoChanges(); throw new IllegalStateException(); } } @Override public void rollback() { if (undoChanges()) { try { callback.operationCancelled(IdentityPatchContext.this); } finally { modification.cancel(); } } } }; } /** * Cancel the current patch and undo the changes. * * @param callback the finalize callback */ protected void cancel(final FinalizeCallback callback) { try { undoChanges(); } finally { callback.operationCancelled(this); } } /** * Complete the current operation and persist the current state to the disk. This will also trigger the invalidation * of outdated modules. * * @param modification the current modification * @param callback the completion callback */ private void complete(final InstallationManager.InstallationModification modification, final FinalizeCallback callback) { final List<File> processed = new ArrayList<File>(); List<File> reenabled = Collections.emptyList(); List<File> disabled = Collections.emptyList(); try { try { // Update the state to invalidate and process module resources if (stateUpdater.compareAndSet(this, State.PREPARED, State.INVALIDATE)) { if (mode == PatchingTaskContext.Mode.APPLY) { // Only invalidate modules when applying patches; on rollback files are immediately restored for (final File invalidation : moduleInvalidations) { processed.add(invalidation); PatchModuleInvalidationUtils.processFile(this, invalidation, mode); } if (!modulesToReenable.isEmpty()) { reenabled = new ArrayList<File>(modulesToReenable.size()); for (final File path : modulesToReenable) { reenabled.add(path); PatchModuleInvalidationUtils.processFile(this, path, PatchingTaskContext.Mode.ROLLBACK); } } } else if(mode == PatchingTaskContext.Mode.ROLLBACK) { if (!modulesToDisable.isEmpty()) { disabled = new ArrayList<File>(modulesToDisable.size()); for (final File path : modulesToDisable) { disabled.add(path); PatchModuleInvalidationUtils.processFile(this, path, PatchingTaskContext.Mode.APPLY); } } } } modification.complete(); callback.completed(this); state = State.COMPLETED; } catch (Exception e) { this.moduleInvalidations.clear(); this.moduleInvalidations.addAll(processed); this.modulesToReenable.clear(); this.modulesToReenable.addAll(reenabled); this.modulesToDisable.clear(); this.moduleInvalidations.addAll(disabled); throw new RuntimeException(e); } } finally { if (state != State.COMPLETED) { try { modification.cancel(); } finally { try { undoChanges(); } finally { callback.operationCancelled(this); } } } else { try { if (checkForGarbageOnRestart) { final File cleanupMarker = new File(installedImage.getInstallationMetadata(), "cleanup-patching-dirs"); cleanupMarker.createNewFile(); } storeFailedRenaming(); } catch (IOException e) { PatchLogger.ROOT_LOGGER.debugf(e, "failed to create cleanup marker"); } } } } private void storeFailedRenaming() throws IOException { if (!renames.isEmpty()) { final File failedRenaming = new File(installedImage.getInstallationMetadata(), "cleanup-renaming-files"); if (!failedRenaming.exists()) { failedRenaming.createNewFile(); } List<String> failures = new ArrayList<String>(renames.keySet()); PatchUtils.writeRefs(failedRenaming, failures, true); } } /** * Internally undo recorded changes we did so far. * * @return whether the state required undo actions */ boolean undoChanges() { final State state = stateUpdater.getAndSet(this, State.ROLLBACK_ONLY); if (state == State.COMPLETED || state == State.ROLLBACK_ONLY) { // Was actually completed already return false; } PatchingTaskContext.Mode currentMode = this.mode; mode = PatchingTaskContext.Mode.UNDO; final PatchContentLoader loader = PatchContentLoader.create(miscBackup, null, null); // Undo changes for the identity undoChanges(identityEntry, loader); // TODO maybe check if we need to do something for the layers too !? if (state == State.INVALIDATE || currentMode == PatchingTaskContext.Mode.ROLLBACK) { // For apply the state needs to be invalidate // For rollback the files are invalidated as part of the tasks final PatchingTaskContext.Mode mode = currentMode == PatchingTaskContext.Mode.APPLY ? PatchingTaskContext.Mode.ROLLBACK : PatchingTaskContext.Mode.APPLY; for (final File file : moduleInvalidations) { try { PatchModuleInvalidationUtils.processFile(this, file, mode); } catch (Exception e) { PatchLogger.ROOT_LOGGER.debugf(e, "failed to restore state for %s", file); } } if(!modulesToReenable.isEmpty()) { for (final File file : modulesToReenable) { try { PatchModuleInvalidationUtils.processFile(this, file, PatchingTaskContext.Mode.APPLY); } catch (Exception e) { PatchLogger.ROOT_LOGGER.debugf(e, "failed to restore state for %s", file); } } } if(!modulesToDisable.isEmpty()) { for (final File file : modulesToDisable) { try { PatchModuleInvalidationUtils.processFile(this, file, PatchingTaskContext.Mode.ROLLBACK); } catch (Exception e) { PatchLogger.ROOT_LOGGER.debugf(e, "failed to restore state for %s", file); } } } } return true; } /** * Undo changes for a single patch entry. * * @param entry the patch entry * @param loader the content loader */ static void undoChanges(final PatchEntry entry, final PatchContentLoader loader) { final List<ContentModification> modifications = new ArrayList<ContentModification>(entry.rollbackActions); for (final ContentModification modification : modifications) { final ContentItem item = modification.getItem(); if (item.getContentType() != ContentType.MISC) { // Skip modules and bundles they should be removed as part of the {@link FinalizeCallback} continue; } final PatchingTaskDescription description = new PatchingTaskDescription(entry.applyPatchId, modification, loader, false, false, false); try { final PatchingTask task = PatchingTask.Factory.create(description, entry); task.execute(entry); } catch (Exception e) { PatchLogger.ROOT_LOGGER.failedToUndoChange(item.toString()); } } } /** * Add a rollback loader for a give patch. * * @param patchId the patch id. * @param target the patchable target * @throws XMLStreamException * @throws IOException */ private void recordRollbackLoader(final String patchId, PatchableTarget.TargetInfo target) { // setup the content loader paths final DirectoryStructure structure = target.getDirectoryStructure(); final InstalledImage image = structure.getInstalledImage(); final File historyDir = image.getPatchHistoryDir(patchId); final File miscRoot = new File(historyDir, PatchContentLoader.MISC); final File modulesRoot = structure.getModulePatchDirectory(patchId); final File bundlesRoot = structure.getBundlesPatchDirectory(patchId); final PatchContentLoader loader = PatchContentLoader.create(miscRoot, bundlesRoot, modulesRoot); // recordContentLoader(patchId, loader); } /** * Record a content loader for a given patch id. * * @param patchID the patch id * @param contentLoader the content loader */ protected void recordContentLoader(final String patchID, final PatchContentLoader contentLoader) { if (contentLoaders.containsKey(patchID)) { throw new IllegalStateException("Content loader already registered for patch " + patchID); // internal wrong usage, no i18n } contentLoaders.put(patchID, contentLoader); } /** * Whether a content verification can be ignored or not. * * @param item the content item to verify * @return */ public boolean isIgnored(final ContentItem item) { return contentPolicy.ignoreContentValidation(item); } /** * Whether a content task execution can be excluded. * * @param item the content item * @return */ public boolean isExcluded(final ContentItem item) { return contentPolicy.preserveExisting(item); } /** * Get the target file for misc items. * * @param item the misc item * @return the target location */ public File getTargetFile(final MiscContentItem item) { final State state = this.state; if (state == State.NEW || state == State.ROLLBACK_ONLY) { return getTargetFile(miscTargetRoot, item); } else { throw new IllegalStateException(); // internal wrong usage, no i18n } } /** * Create a patch representing what we actually processed. This may contain some fixed content hashes for removed * modules. * * @param original the original * @return the processed patch */ protected Patch createProcessedPatch(final Patch original) { // Process elements final List<PatchElement> elements = new ArrayList<PatchElement>(); // Process layers for (final PatchEntry entry : getLayers()) { final PatchElement element = createPatchElement(entry, entry.element.getId(), entry.modifications); elements.add(element); } // Process add-ons for (final PatchEntry entry : getAddOns()) { final PatchElement element = createPatchElement(entry, entry.element.getId(), entry.modifications); elements.add(element); } // Swap the patch element modifications, keep the identity ones since we don't need to fix the misc modifications return new PatchImpl(original.getPatchId(), original.getDescription(), original.getLink(), original.getIdentity(), elements, identityEntry.modifications); } /** * Create a rollback patch based on the recorded actions. * * @param patchId the new patch id, depending on release or one-off * @param patchType the current patch identity * @return the rollback patch */ protected RollbackPatch createRollbackPatch(final String patchId, final Patch.PatchType patchType) { // Process elements final List<PatchElement> elements = new ArrayList<PatchElement>(); // Process layers for (final PatchEntry entry : getLayers()) { final PatchElement element = createRollbackElement(entry); elements.add(element); } // Process add-ons for (final PatchEntry entry : getAddOns()) { final PatchElement element = createRollbackElement(entry); elements.add(element); } final InstalledIdentity installedIdentity = modification.getUnmodifiedInstallationState(); final String name = installedIdentity.getIdentity().getName(); final IdentityImpl identity = new IdentityImpl(name, modification.getVersion()); if (patchType == Patch.PatchType.CUMULATIVE) { identity.setPatchType(Patch.PatchType.CUMULATIVE); identity.setResultingVersion(installedIdentity.getIdentity().getVersion()); } else if (patchType == Patch.PatchType.ONE_OFF) { identity.setPatchType(Patch.PatchType.ONE_OFF); } final List<ContentModification> modifications = identityEntry.rollbackActions; final Patch delegate = new PatchImpl(patchId, "rollback patch", identity, elements, modifications); return new PatchImpl.RollbackPatchImpl(delegate, installedIdentity); } /** * Get a misc file. * * @param root the root * @param item the misc content item * @return the misc file */ static File getTargetFile(final File root, final MiscContentItem item) { return PatchContentLoader.getMiscPath(root, item); } class IdentityEntry extends PatchEntry { IdentityEntry(InstallationManager.MutablePatchingTarget delegate, PatchElement element) { super(delegate, element); } @Override protected String getResultingVersion() { return modification.getVersion(); } @Override public void setResultingVersion(String resultingVersion) { modification.setResultingVersion(resultingVersion); } } /** * Modification information for a patchable target. */ class PatchEntry extends ContentTaskDefinitions implements InstallationManager.MutablePatchingTarget, PatchingTaskContext { private String applyPatchId; private PatchElement element; private final InstallationManager.MutablePatchingTarget delegate; private final List<ContentModification> modifications = new ArrayList<ContentModification>(); private final List<ContentModification> rollbackActions = new ArrayList<ContentModification>(); private final Set<String> rollbacks = new HashSet<String>(); PatchEntry(final InstallationManager.MutablePatchingTarget delegate, final PatchElement element) { assert delegate != null; this.delegate = delegate; this.element = element; } protected void updateElement(final PatchElement element) { this.element = element; } protected String getResultingVersion() { throw new IllegalStateException(); } public void setResultingVersion(String resultingVersion) { throw new IllegalStateException(); } @Override public boolean isApplied(String patchId) { return delegate.isApplied(patchId); } @Override public void rollback(String patchId) { rollbacks.add(patchId); // Rollback delegate.rollback(patchId); // Record rollback loader recordRollbackLoader(patchId, delegate); } @Override public boolean isRolledback(String patchId) { return rollbacks.contains(patchId); } @Override public void apply(String patchId, Patch.PatchType patchType) { delegate.apply(patchId, patchType); applyPatchId = patchId; } @Override public String getCumulativePatchID() { return delegate.getCumulativePatchID(); } @Override public List<String> getPatchIDs() { return delegate.getPatchIDs(); } @Override public Properties getProperties() { return delegate.getProperties(); } @Override public DirectoryStructure getDirectoryStructure() { return delegate.getDirectoryStructure(); } @Override public File getBackupFile(MiscContentItem item) { if (state == State.NEW) { return IdentityPatchContext.getTargetFile(miscBackup, item); } else if (state == State.ROLLBACK_ONLY) { // No backup when we undo the changes return null; } else { throw new IllegalStateException(); // internal wrong usage, no i18n } } @Override public boolean isExcluded(ContentItem contentItem) { return contentPolicy.preserveExisting(contentItem); } @Override public void recordChange(final ContentModification change, final ContentModification rollbackAction) { if (state == State.ROLLBACK_ONLY) { // don't record undo tasks return; } // Only misc remove is null, but we replace it with the if (change != null) { modifications.add(change); } if (rollbackAction != null) { rollbackActions.add(rollbackAction); } } @Override public Mode getCurrentMode() { return mode; } @Override public PatchableTarget.TargetInfo getModifiedState() { return delegate.getModifiedState(); } @Override public File[] getTargetBundlePath() { // We need the updated state for invalidating one-off patches // When applying the overlay directory should not exist yet final PatchableTarget.TargetInfo updated = mode == Mode.APPLY ? delegate : delegate.getModifiedState(); return PatchUtils.getBundlePath(delegate.getDirectoryStructure(), updated); } @Override public File[] getTargetModulePath() { // We need the updated state for invalidating one-off patches // When applying the overlay directory should not exist yet final PatchableTarget.TargetInfo updated = mode == Mode.APPLY ? delegate : delegate.getModifiedState(); return PatchUtils.getModulePath(delegate.getDirectoryStructure(), updated); } @Override public File getTargetFile(ContentItem item) { if (item.getContentType() == ContentType.MISC) { return IdentityPatchContext.this.getTargetFile((MiscContentItem) item); } if (applyPatchId == null || state == State.ROLLBACK_ONLY) { throw new IllegalStateException("cannot process rollback tasks for modules/bundles"); // internal wrong usage, no i18n } final File root; final DirectoryStructure structure = delegate.getDirectoryStructure(); if (item.getContentType() == ContentType.BUNDLE) { root = structure.getBundlesPatchDirectory(applyPatchId); } else { root = structure.getModulePatchDirectory(applyPatchId); } return PatchContentLoader.getModulePath(root, (ModuleItem) item); } @Override public void invalidateRoot(final File moduleRoot) throws IOException { final List<File> files = listFiles(moduleRoot); if (files != null && files.size() > 0) { for (final File file : files) { moduleInvalidations.add(file); if (mode == Mode.ROLLBACK) { // For rollback we need to restore the file before calculating the hash PatchModuleInvalidationUtils.processFile(null, file, mode); } } } } void prepareForPortForward(ContentItem item, String patchId) throws IOException { if (item.getContentType() == ContentType.MODULE) { final File targetFile = delegate.getDirectoryStructure().getModulePatchDirectory(patchId); final List<File> files = listFiles(targetFile); if (files != null && files.size() > 0) { for (final File file : files) { moduleInvalidations.add(file); PatchModuleInvalidationUtils.processFile(IdentityPatchContext.this, file, PatchingTaskContext.Mode.ROLLBACK); } } } } void reenableBaseModule(final ModuleItem item) throws IOException { if(modulesToReenable.isEmpty()) { modulesToReenable = new ArrayList<File>(); } final File modulePath = PatchContentLoader.getModulePath(getDirectoryStructure().getModuleRoot(), item.getName(), ((ModuleItem)item).getSlot()); final List<File> files = listFiles(modulePath); if (files != null && files.size() > 0) { for (final File file : files) { modulesToReenable.add(file); } } } void disableBaseModule(final ModuleItem item) throws IOException { if(modulesToDisable.isEmpty()) { modulesToDisable = new ArrayList<File>(); } final File modulePath = PatchContentLoader.getModulePath(getDirectoryStructure().getModuleRoot(), item.getName(), ((ModuleItem)item).getSlot()); final List<File> files = listFiles(modulePath); if (files != null && files.size() > 0) { for (final File file : files) { modulesToDisable.add(file); } } } protected List<File> listFiles(final File... files) { final List<File> result = new ArrayList<File>(); for (File f : files) { final String name = f.getName(); if (f.isFile() && (name.endsWith(JAR_EXT) || name.endsWith(BACKUP_EXT))) { result.add(f); } else if (f.isDirectory()) { result.addAll(listFiles(f.listFiles())); } } return result; } /** * Cleanup the history directories for all recorded rolled back patches. */ protected void cleanupRollbackPatchHistory() { final DirectoryStructure structure = getDirectoryStructure(); for (final String rollback : rollbacks) { if (!IoUtils.recursiveDelete(structure.getBundlesPatchDirectory(rollback))) { failedToCleanupDir(structure.getBundlesPatchDirectory(rollback)); } if (!IoUtils.recursiveDelete(structure.getModulePatchDirectory(rollback))) { failedToCleanupDir(structure.getModulePatchDirectory(rollback)); } } } } /** * Patch finalization callback. */ interface FinalizeCallback { /** * Get the original patch. * * @return the patch */ Patch getPatch(); /** * Finish step after the content modification were executed. * * @param processedPatch the processed patch * @param rollbackPatch the rollback patch * @param context the patch context * @throws Exception */ void finishPatch(Patch processedPatch, RollbackPatch rollbackPatch, IdentityPatchContext context) throws Exception; /** * Completed. * * @param context the context */ void completed(IdentityPatchContext context); /** * Cancelled. * * @param context the context */ void operationCancelled(IdentityPatchContext context); } /** * Create a patch element for the rollback patch. * * @param entry the entry * @return the new patch element */ protected static PatchElement createRollbackElement(final PatchEntry entry) { final PatchElement patchElement = entry.element; final String patchId; final Patch.PatchType patchType = patchElement.getProvider().getPatchType(); if (patchType == Patch.PatchType.CUMULATIVE) { patchId = entry.getCumulativePatchID(); } else { patchId = patchElement.getId(); } return createPatchElement(entry, patchId, entry.rollbackActions); } /** * Copy a patch element * * @param entry the patch entry * @param patchId the patch id for the element * @param modifications the element modifications * @return the new patch element */ protected static PatchElement createPatchElement(final PatchEntry entry, String patchId, final List<ContentModification> modifications) { final PatchElement patchElement = entry.element; final PatchElementImpl element = new PatchElementImpl(patchId); element.setProvider(patchElement.getProvider()); // Add all the rollback actions element.getModifications().addAll(modifications); element.setDescription(patchElement.getDescription()); return element; } /** * Backup the current configuration as part of the patch history. * * @throws IOException for any error */ void backupConfiguration() throws IOException { final String configuration = Constants.CONFIGURATION; final File a = new File(installedImage.getAppClientDir(), configuration); final File d = new File(installedImage.getDomainDir(), configuration); final File s = new File(installedImage.getStandaloneDir(), configuration); if (a.exists()) { final File ab = new File(configBackup, Constants.APP_CLIENT); backupDirectory(a, ab); } if (d.exists()) { final File db = new File(configBackup, Constants.DOMAIN); backupDirectory(d, db); } if (s.exists()) { final File sb = new File(configBackup, Constants.STANDALONE); backupDirectory(s, sb); } } static final FileFilter CONFIG_FILTER = new FileFilter() { @Override public boolean accept(File pathName) { return pathName.isFile() && pathName.getName().endsWith(".xml"); } }; /** * Backup all xml files in a given directory. * * @param source the source directory * @param target the target directory * @throws IOException for any error */ static void backupDirectory(final File source, final File target) throws IOException { if (!target.exists()) { if (!target.mkdirs()) { throw PatchLogger.ROOT_LOGGER.cannotCreateDirectory(target.getAbsolutePath()); } } final File[] files = source.listFiles(CONFIG_FILTER); for (final File file : files) { final File t = new File(target, file.getName()); IoUtils.copyFile(file, t); } } /** * Restore the configuration. Depending on reset-configuration this is going to replace the original files with the * backup, otherwise it will create a restored-configuration folder the configuration directories. * <p/> * TODO log a warning if the restored configuration files are different from the current one? * or should we check that before rolling back the patch to give the user a chance to save the changes * * @param rollingBackPatchID the patch id * @param resetConfiguration whether to override the configuration files or not * @throws IOException for any error */ void restoreConfiguration(final String rollingBackPatchID, final boolean resetConfiguration) throws IOException { final File backupConfigurationDir = new File(installedImage.getPatchHistoryDir(rollingBackPatchID), Constants.CONFIGURATION); final File ba = new File(backupConfigurationDir, Constants.APP_CLIENT); final File bd = new File(backupConfigurationDir, Constants.DOMAIN); final File bs = new File(backupConfigurationDir, Constants.STANDALONE); final String configuration; if (resetConfiguration) { configuration = Constants.CONFIGURATION; } else { configuration = Constants.CONFIGURATION + File.separator + Constants.RESTORED_CONFIGURATION; } if (ba.exists()) { final File a = new File(installedImage.getAppClientDir(), configuration); backupDirectory(ba, a); } if (bd.exists()) { final File d = new File(installedImage.getDomainDir(), configuration); backupDirectory(bd, d); } if (bs.exists()) { final File s = new File(installedImage.getStandaloneDir(), configuration); backupDirectory(bs, s); } } /** * Write the patch.xml * * @param rollbackPatch the patch * @param file the target file * @throws IOException */ static void writePatch(final Patch rollbackPatch, final File file) throws IOException { final File parent = file.getParentFile(); if (!parent.isDirectory()) { if (!parent.mkdirs() && !parent.exists()) { throw PatchLogger.ROOT_LOGGER.cannotCreateDirectory(file.getAbsolutePath()); } } try { try (final OutputStream os = new FileOutputStream(file)){ PatchXml.marshal(os, rollbackPatch); } } catch (XMLStreamException e) { throw new IOException(e); } } }