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);
}
}
}