/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.patching.runner;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jboss.as.patching.metadata.ContentItem;
import org.jboss.as.patching.metadata.ContentModification;
import org.jboss.as.patching.metadata.ModificationType;
import org.jboss.as.patching.runner.IdentityPatchContext.PatchEntry;
/**
* Utility class to auto resolve conflicts when rolling back/invalidating patches.
*
* The base for determining whether a release or cumulative patch can be applied cleanly (no conflicts) is when all one-patches
* have been rolled back. For a rollback we go through the recorded history and update the target (current state), so
* that we are left with a single transition. Additionally we track whether conflicts had to be resolved when applying
* a previous patch and mark this as a conflict to be resolved when applying this patch.
*
* @author Emanuel Muckenhuber
*/
class PatchingTasks {
static final EnumSet<ModificationType> ALL_MODIFICATIONS = EnumSet.allOf(ModificationType.class);
static final EnumSet<ModificationType> ALL_BUT_MODIFY = EnumSet.of(ModificationType.ADD, ModificationType.REMOVE);
/**
* Process multiple patches for rollback, trying to determine the current and target state for this applying this combination.
* <p/>
* This will track changes by location, trying to merge multiple changes per location and recording whether conflicts
* were detected or not. It's up to the user to skip the content validation steps.
* </p>
* Compare the recorded rollback history and the original patch - to detect conflicts.
*
* @param patchId the patch id
* @param originalPatch the original modifications
* @param rollbackPatch the rollback modifications
* @param modifications the definitions
* @param filter the content filter
* @param mode the current patching mode
*/
static void rollback(final String patchId, final Collection<ContentModification> originalPatch, final Collection<ContentModification> rollbackPatch,
final ContentTaskDefinitions modifications, final ContentItemFilter filter,
final PatchingTaskContext.Mode mode) {
// Process the original patch information
final Map<Location, ContentModification> originalModifications = new HashMap<Location, ContentModification>(originalPatch.size());
for (final ContentModification modification : originalPatch) {
originalModifications.put(new Location(modification.getItem()), modification);
}
// Process the rollback information
for (final ContentModification modification : rollbackPatch) {
final ContentItem item = modification.getItem();
// Skip module items when rolling back
if (!filter.accepts(item)) {
continue;
}
final Location location = new Location(item);
final ContentModification original = originalModifications.remove(location);
final ContentEntry contentEntry = new ContentEntry(patchId, modification);
ContentTaskDefinition definition = modifications.get(location);
if (definition == null) {
definition = new ContentTaskDefinition(location, contentEntry, true);
modifications.put(location, definition);
} else {
// TODO perhaps we don't need check that
boolean strict = true; // Strict history checks
if (strict) {
// Check if the consistency of the history
final ContentEntry previous = definition.getTarget();
final byte[] hash = previous.getItem().getContentHash();
if (!Arrays.equals(hash, contentEntry.getTargetHash())) {
throw new IllegalStateException();
}
}
//
definition.setTarget(contentEntry);
}
if (original == null
|| mode == PatchingTaskContext.Mode.ROLLBACK) {
continue;
}
// Check if the current content was the original item (preserve)
final byte[] currentContent = modification.getTargetHash();
final byte[] originalContent = original.getItem().getContentHash();
// TODO relax requirements for conflict resolution on rollback!
if (!Arrays.equals(currentContent, originalContent)) {
definition.addConflict(contentEntry);
} else {
// Check if backup item was the targeted one (override)
final byte[] backupItem = item.getContentHash();
final byte[] originalTarget = original.getTargetHash();
//
if (!Arrays.equals(backupItem, originalTarget)) {
definition.addConflict(contentEntry);
}
}
}
}
static void addMissingModifications(IdentityPatchContext.PatchEntry target, Collection<ContentModification> modifications, final ContentItemFilter filter) throws IOException {
final String cpId = target.getCumulativePatchID();
for (final ContentModification modification : modifications) {
final ContentItem item = modification.getItem();
// Check if we accept the item
if (!filter.accepts(item)) {
continue;
}
final Location location = new Location(item);
final ContentTaskDefinition definition = target.get(location);
if (definition == null) {
target.put(location, new ContentTaskDefinition(location, new ContentEntry(cpId, modification), false));
} else if(definition.isRollback()) {
target.prepareForPortForward(item, cpId);
definition.setTarget(new ContentEntry(cpId, modification));
}
}
}
static void apply(final String patchId, final Collection<ContentModification> modifications, final PatchEntry patchEntry) {
apply(patchId, modifications, patchEntry, ContentItemFilter.ALL);
}
/**
* Apply modifications to a content task definition.
*
* @param patchId the patch id
* @param modifications the modifications
* @param definitions the task definitions
* @param filter the content item filter
*/
static void apply(final String patchId, final Collection<ContentModification> modifications, final PatchEntry patchEntry, final ContentItemFilter filter) {
for (final ContentModification modification : modifications) {
final ContentItem item = modification.getItem();
// Check if we accept the item
if (!filter.accepts(item)) {
continue;
}
final Location location = new Location(item);
final ContentEntry contentEntry = new ContentEntry(patchId, modification);
ContentTaskDefinition definition = patchEntry.get(location);
if (definition == null) {
definition = new ContentTaskDefinition(location, contentEntry, false);
patchEntry.put(location, definition);
} else {
definition.setTarget(contentEntry);
}
}
}
static class ContentTaskDefinition {
private final Location location;
private final ContentEntry latest;
private ContentEntry target;
private boolean rollback;
private final List<ContentEntry> conflicts = new ArrayList<ContentEntry>();
ContentTaskDefinition(Location location, ContentEntry latest, boolean rollback) {
this.location = location;
this.latest = latest;
this.target = latest;
this.rollback = rollback;
}
public boolean isRollback() {
return rollback;
}
public Location getLocation() {
return location;
}
public ContentEntry getLatest() {
return latest;
}
public ContentEntry getTarget() {
return target;
}
public boolean hasConflicts() {
return !conflicts.isEmpty();
}
public List<ContentEntry> getConflicts() {
return conflicts;
}
void setTarget(final ContentEntry entry) {
target = entry;
rollback = false;
}
void addConflict(ContentEntry entry) {
conflicts.add(entry);
}
}
static class ContentEntry {
final String patchId;
final ContentModification modification;
ContentEntry(String patchId, ContentModification modification) {
this.patchId = patchId;
this.modification = modification;
}
public String getPatchId() {
return patchId;
}
public ContentModification getModification() {
return modification;
}
public ContentItem getItem() {
return modification.getItem();
}
public byte[] getTargetHash() {
return modification.getTargetHash();
}
}
}