/*
* JBoss, Home of Professional Open Source.
* Copyright 2015, 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.metadata;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.jboss.as.patching.Constants;
import org.jboss.as.patching.IoUtils;
import org.jboss.as.patching.PatchingException;
import org.jboss.as.patching.ZipUtils;
import org.jboss.as.patching.logging.PatchLogger;
import org.jboss.as.patching.metadata.Patch.PatchType;
import org.jboss.as.patching.runner.PatchUtils;
import org.wildfly.security.manager.WildFlySecurityManager;
/**
*
* @author Alexey Loubyansky
*/
public class PatchMerger {
private static final String DIRECTORY_PREFIX = "tmp-";
public static final String PATCH_XML_SUFFIX = "-patch.xml";
private static final File TEMP_DIR = new File(WildFlySecurityManager.getPropertyPrivileged("java.io.tmpdir", null));
public static File merge(File patch1, File patch2, File merged) throws PatchingException {
final File workDir = createTempDir();
final File patch1Dir = expandContent(patch1, workDir, "patch1");
final File patch2Dir = expandContent(patch2, workDir, "patch2");
final File mergedDir = new File(workDir, "merged");
final Patch patch1Metadata = parsePatchXml(patch1Dir, patch1);
final Patch patch2Metadata = parsePatchXml(patch2Dir, patch2);
final Patch mergedMetadata = merge(patch1Metadata, patch2Metadata);
// list(patch1Dir);
// list(patch2Dir);
if (!mergedDir.mkdirs()) {
throw new PatchingException("Failed to create directory " + mergedDir.getAbsolutePath());
}
// merge with the previous versions
for (File f : patch1Dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(PATCH_XML_SUFFIX);
}
})) {
Patch patch;
try {
patch = PatchXml.parse(f).resolvePatch(null, null);
} catch (Exception e) {
throw new PatchingException("Failed to parse " + f.getAbsolutePath(), e);
}
patch = merge(patch, patch2Metadata);
try (Writer writer = Files.newBufferedWriter(new File(mergedDir, f.getName()).toPath(), StandardCharsets.UTF_8)){
PatchXml.marshal(writer, patch);
} catch (Exception e) {
throw new PatchingException("Failed to marshal merged metadata into " + f.getName(), e);
}
}
// the latest patch.xml
copyFile(new File(patch2Dir, PatchXml.PATCH_XML), new File(mergedDir, patch2Metadata.getIdentity().getVersion() + PATCH_XML_SUFFIX));
// merged patch.xml is the metadata from the earliest version to the latest
try (Writer writer = Files.newBufferedWriter(new File(mergedDir, PatchXml.PATCH_XML).toPath(), StandardCharsets.UTF_8)){
PatchXml.marshal(writer, mergedMetadata);
} catch (Exception e) {
throw new PatchingException("Failed to marshal merged metadata into " + PatchXml.PATCH_XML, e);
}
try {
mergeRootContent(new File(patch1Dir, patch1Metadata.getPatchId()),
new File(patch2Dir, patch2Metadata.getPatchId()), new File(mergedDir, patch2Metadata.getPatchId()));
} catch (IOException e) {
throw new PatchingException("Failed to merge root modifications", e);
}
try {
mergeElementContent(patch1Dir, patch2Dir, mergedDir, patch1Metadata, patch2Metadata);
} catch (IOException e) {
throw new PatchingException("Failed to merge element modifications", e);
}
// list(mergedDir);
ZipUtils.zip(mergedDir, merged);
IoUtils.recursiveDelete(workDir);
return merged;
}
private static void copyFile(File source, File target) throws PatchingException {
try {
Files.copy(source.toPath(), target.toPath());
} catch (IOException e1) {
throw new PatchingException("Failed to copy " + source.getAbsolutePath() + " to " + target.getAbsolutePath());
}
}
private static void mergeElementContent(final File patch1Dir, final File patch2Dir, final File mergedDir,
final Patch patch1Metadata, final Patch patch2Metadata) throws PatchingException, IOException {
final Map<String, PatchElement> patch2Elements = new HashMap<String, PatchElement>(patch2Metadata.getElements().size());
for (PatchElement e : patch2Metadata.getElements()) {
patch2Elements.put(e.getProvider().getName(), e);
}
for (PatchElement e1 : patch1Metadata.getElements()) {
final File e1Dir = new File(patch1Dir, e1.getId());
final PatchElement e2 = patch2Elements.remove(e1.getProvider().getName());
if (e2 == null) {
if (e1Dir.exists()) {
IoUtils.copyFile(e1Dir, new File(mergedDir, e1.getId()));
}
} else {
final ContentModifications cp2Mods = new ContentModifications(e2.getModifications());
for (ContentModification cp1Mod : e1.getModifications()) {
final ContentModification cp2Mod = cp2Mods.remove(cp1Mod.getItem());
if (cp2Mod == null) {
copyModificationContent(patch1Dir, e1.getId(), mergedDir, e2.getId(), cp1Mod);
} else {
copyModificationContent(patch2Dir, e2.getId(), mergedDir, e2.getId(), cp2Mod);
}
}
for(ContentModification cp2Mod : cp2Mods.getModifications()) {
copyModificationContent(patch2Dir, e2.getId(), mergedDir, e2.getId(), cp2Mod);
}
}
}
for (PatchElement e2 : patch2Elements.values()) {
final File e2Dir = new File(patch2Dir, e2.getId());
if (e2Dir.exists()) {
IoUtils.copyFile(e2Dir, new File(mergedDir, e2.getId()));
}
}
}
private static void copyModificationContent(final File srcPatchDir, final String srcElementId,
final File targetPatchDir, final String targetElementId,
final ContentModification mod) throws PatchingException {
final ModificationType type = mod.getType();
if(type.equals(ModificationType.REMOVE)) {
return;
}
File modSrcDir = new File(srcPatchDir, srcElementId);
File modTrgDir = new File(targetPatchDir, targetElementId);
final ContentType contentType = mod.getItem().getContentType();
if(ContentType.MISC.equals(contentType)) {
modSrcDir = new File(modSrcDir, Constants.MISC);
modTrgDir = new File(modTrgDir, Constants.MISC);
for (final String path : ((MiscContentItem)mod.getItem()).getPath()) {
modSrcDir = new File(modSrcDir, path);
modTrgDir = new File(modTrgDir, path);
}
copyDir(modSrcDir, modTrgDir);
} else {
final String slot;
if (contentType.equals(ContentType.MODULE)) {
modSrcDir = new File(modSrcDir, Constants.MODULES);
modTrgDir = new File(modTrgDir, Constants.MODULES);
slot = ((ModuleItem)mod.getItem()).getSlot();
} else if (contentType.equals(ContentType.BUNDLE)) {
modSrcDir = new File(modSrcDir, Constants.BUNDLES);
modTrgDir = new File(modTrgDir, Constants.BUNDLES);
slot = ((BundleItem)mod.getItem()).getSlot();
} else {
throw new PatchingException("Unexpected content type " + contentType);
}
for (String name : mod.getItem().getName().split("\\.")) {
modSrcDir = new File(modSrcDir, name);
modTrgDir = new File(modTrgDir, name);
}
modSrcDir = new File(modSrcDir, slot);
modTrgDir = new File(modTrgDir, slot);
copyDir(modSrcDir, modTrgDir);
}
}
protected static void copyDir(File src, File trg) throws PatchingException {
try {
IoUtils.copyFile(src, trg);
} catch (IOException e) {
throw new PatchingException("Failed to copy modification content from " + src.getAbsolutePath() + " to " + trg.getAbsolutePath(), e);
}
}
private static void mergeRootContent(final File root1Dir, final File root2Dir, final File mergedRootDir) throws IOException {
if (root1Dir.exists()) {
IoUtils.copyFile(root1Dir, mergedRootDir);
}
if (root2Dir.exists()) {
IoUtils.copyFile(root2Dir, mergedRootDir);
}
}
private static Patch parsePatchXml(final File patch1Dir, File patch1) throws PatchingException {
final File patch1Xml = new File(patch1Dir, PatchXml.PATCH_XML);
if (!patch1Xml.exists()) {
throw new PatchingException("Failed to locate " + PatchXml.PATCH_XML + " in " + patch1.getAbsolutePath());
}
try {
return PatchXml.parse(patch1Xml).resolvePatch(null, null);
} catch (Exception e) {
throw new PatchingException("Failed to parse " + PatchXml.PATCH_XML + " from " + patch1.getAbsolutePath(), e);
}
}
private static File expandContent(File patchFile, final File workDir, final String expandDirName) throws PatchingException {
final File patchDir;
try {
if (!patchFile.isDirectory()) {
patchDir = new File(workDir, expandDirName);
// Save the content
final File cachedContent = new File(patchDir, "content");
IoUtils.copy(patchFile, cachedContent);
// Unpack to the work dir
ZipUtils.unzip(cachedContent, patchDir);
} else {
patchDir = patchFile;
}
} catch (IOException e) {
throw new PatchingException("Failed to unzip " + patchFile.getAbsolutePath());
}
return patchDir;
}
public static Patch merge(Patch cp1, Patch cp2) throws PatchingException {
return merge(cp1, cp2, true);
}
public static Patch merge(Patch cp1, Patch cp2, boolean nextVersion) throws PatchingException {
// for now support merging only CPs
final Identity.IdentityUpgrade cp1Identity = cp1.getIdentity().forType(Patch.PatchType.CUMULATIVE,
Identity.IdentityUpgrade.class);
final Identity.IdentityUpgrade cp2Identity = cp2.getIdentity().forType(Patch.PatchType.CUMULATIVE,
Identity.IdentityUpgrade.class);
assertUpgrade(cp1Identity.getPatchType());
assertUpgrade(cp2Identity.getPatchType());
// for now support merging only CPs targeting the same identity name
if (!cp1Identity.getName().equals(cp2Identity.getName())) {
throw new PatchingException("Patches target different identities: " + cp1Identity.getName() + " and "
+ cp2Identity.getName());
}
if (nextVersion && !cp1Identity.getResultingVersion().equals(cp2Identity.getVersion())) {
throw new PatchingException(cp1.getPatchId() + " upgrades to version " + cp1Identity.getResultingVersion()
+ " but " + cp2.getPatchId() + " targets version " + cp2Identity.getVersion());
}
final PatchBuilder builder = PatchBuilder.create().setPatchId(cp2.getPatchId()).setDescription(cp2.getDescription())
.setLink(cp2.getLink());
builder.upgradeIdentity(cp1Identity.getName(), cp1Identity.getVersion(), cp2Identity.getResultingVersion());
final Map<String, PatchElement> cp2LayerElements = new HashMap<String, PatchElement>();
final Map<String, PatchElement> cp2AddonElements = new HashMap<String, PatchElement>();
for (PatchElement pe : cp2.getElements()) {
final PatchElementProvider provider = pe.getProvider();
assertUpgrade(provider.getPatchType());
if (provider.isAddOn()) {
cp2AddonElements.put(provider.getName(), pe);
} else {
cp2LayerElements.put(provider.getName(), pe);
}
}
for (final PatchElement cp1El : cp1.getElements()) {
final PatchElementProvider provider = cp1El.getProvider();
assertUpgrade(provider.getPatchType());
final PatchElement cp2El;
if (provider.isAddOn()) {
cp2El = cp2AddonElements.remove(provider.getName());
} else {
cp2El = cp2LayerElements.remove(provider.getName());
}
if (cp2El == null) {
builder.addElement(cp1El);
} else {
final PatchElementBuilder elementBuilder = builder.upgradeElement(cp2El.getId(), provider.getName(),
provider.isAddOn()).setDescription(cp2El.getDescription());
mergeModifications(elementBuilder, cp1El.getModifications(), cp2El.getModifications(), cp1, cp2);
}
}
for (PatchElement cp2Element : cp2LayerElements.values()) {
builder.addElement(cp2Element);
}
for (PatchElement cp2Element : cp2AddonElements.values()) {
builder.addElement(cp2Element);
}
mergeModifications(builder, cp1.getModifications(), cp2.getModifications(), cp1, cp2);
return builder.build();
}
private static void mergeModifications(final ModificationBuilderTarget<?> elementBuilder,
Collection<ContentModification> cp1Modifications, Collection<ContentModification> cp2Modifications, Patch cp1,
Patch cp2) throws PatchingException {
final ContentModifications cp2Mods = new ContentModifications(cp2Modifications);
for (ContentModification cp1Mod : cp1Modifications) {
final ContentModification cp2Mod = cp2Mods.remove(cp1Mod.getItem());
if (cp2Mod == null) {
elementBuilder.addContentModification(cp1Mod);
} else {
final ModificationType cp1Type = cp1Mod.getType();
final ModificationType cp2Type = cp2Mod.getType();
final ModificationType modType;
if (cp1Type.equals(ModificationType.ADD)) {
if (cp2Type.equals(ModificationType.ADD)) {
throw new PatchingException("Patch " + cp2.getPatchId() + " adds " + cp1Mod.getItem().getRelativePath()
+ " already added by patch " + cp1.getPatchId());
}
if (cp2Type.equals(ModificationType.MODIFY)) {
modType = ModificationType.ADD;
} else { // remove cancels add
if(cp1Mod.getItem().getContentType().equals(ContentType.MODULE)) {
// but not for modules where remove is effectively modify (resulting in module.xml indicating an absent module)
// so add becomes an add of an absent module
modType = ModificationType.ADD;
} else {
modType = null;
continue;
}
}
} else if (cp1Type.equals(ModificationType.REMOVE)) {
if (cp2Type.equals(ModificationType.REMOVE)) {
throw new PatchingException("Patch " + cp2.getPatchId() + " removes "
+ cp1Mod.getItem().getRelativePath() + " already removed by patch " + cp1.getPatchId());
}
/*if (cp2Type.equals(ModificationType.MODIFY)) {
throw new PatchingException("Patch " + cp2.getPatchId() + " modifies "
+ cp1Mod.getItem().getRelativePath() + " removed by patch " + cp1.getPatchId());
this could happen since the REMOVE will leave a module.xml indicating the module is absent
so, to re-add the module, it has to be MODIFY, since ADD will fail
}*/
// add after remove makes it modify
modType = ModificationType.MODIFY;
} else { // modify
if (cp2Type.equals(ModificationType.ADD)) {
throw new PatchingException("Patch " + cp2.getPatchId() + " adds " + cp1Mod.getItem().getRelativePath()
+ " modified by patch " + cp1.getPatchId());
}
if (cp2Type.equals(ModificationType.REMOVE)) {
modType = ModificationType.REMOVE;
} else {
modType = ModificationType.MODIFY;
}
}
if (ModificationType.ADD.equals(modType)) {
final ContentItem cp2Item = cp2Mod.getItem();
if (cp2Item.getContentType().equals(ContentType.MODULE)) {
final ModuleItem module = (ModuleItem) cp2Item;
if(cp2Type.equals(ModificationType.REMOVE)) {
try {
elementBuilder.addModule(module.getName(), module.getSlot(), PatchUtils.getAbsentModuleContentHash(module));
} catch (IOException e) {
throw new PatchingException("Failed to calculate hash for the removed module " + module.getName(), e);
}
} else {
elementBuilder.addModule(module.getName(), module.getSlot(), module.getContentHash());
}
} else if (cp2Item.getContentType().equals(ContentType.MISC)) {
final MiscContentItem misc = (MiscContentItem) cp2Item;
elementBuilder.addFile(misc.getName(), Arrays.asList(misc.getPath()), misc.getContentHash(),
misc.isDirectory());
} else { // bundle
final BundleItem bundle = (BundleItem) cp2Item;
elementBuilder.addBundle(bundle.getName(), bundle.getSlot(), bundle.getContentHash());
}
} else if (ModificationType.REMOVE.equals(modType)) {
final ContentItem cp1Item = cp1Mod.getItem();
if (cp1Item.getContentType().equals(ContentType.MODULE)) {
final ModuleItem module = (ModuleItem) cp2Mod.getItem();
elementBuilder.removeModule(module.getName(), module.getSlot(), cp1Mod.getTargetHash());
} else if (cp1Item.getContentType().equals(ContentType.MISC)) {
final MiscContentItem misc = (MiscContentItem) cp2Mod.getItem();
elementBuilder.removeFile(misc.getName(), Arrays.asList(misc.getPath()), cp1Mod.getTargetHash(),
misc.isDirectory());
} else { // bundle
final BundleItem bundle = (BundleItem) cp2Mod.getItem();
elementBuilder.removeBundle(bundle.getName(), bundle.getSlot(), cp1Mod.getTargetHash());
}
} else { // modify
final ContentItem cp1Item = cp1Mod.getItem();
if (cp1Item.getContentType().equals(ContentType.MODULE)) {
final ModuleItem module = (ModuleItem) cp2Mod.getItem();
elementBuilder.modifyModule(module.getName(), module.getSlot(), cp1Mod.getTargetHash(),
module.getContentHash());
} else if (cp1Item.getContentType().equals(ContentType.MISC)) {
final MiscContentItem misc = (MiscContentItem) cp2Mod.getItem();
elementBuilder.modifyFile(misc.getName(), Arrays.asList(misc.getPath()), cp1Mod.getTargetHash(),
misc.getContentHash(), misc.isDirectory());
} else { // bundle
final BundleItem bundle = (BundleItem) cp2Mod.getItem();
elementBuilder.modifyBundle(bundle.getName(), bundle.getSlot(), cp1Mod.getTargetHash(),
bundle.getContentHash());
}
}
}
}
for (ContentModification cp2Mod : cp2Mods.getModifications()) {
elementBuilder.addContentModification(cp2Mod);
}
}
private static void assertUpgrade(final PatchType patchType) throws PatchingException {
if (!PatchType.CUMULATIVE.equals(patchType)) {
throw new PatchingException("Merging one-off patches is not supported at this point.");
}
}
static File createTempDir() throws PatchingException {
return createTempDir(TEMP_DIR);
}
static File createTempDir(final File parent) throws PatchingException {
File workDir = null;
int count = 0;
while (workDir == null || workDir.exists()) {
count++;
workDir = new File(parent == null ? TEMP_DIR : parent, DIRECTORY_PREFIX + count);
}
if (!workDir.mkdirs()) {
throw new PatchingException(PatchLogger.ROOT_LOGGER.cannotCreateDirectory(workDir.getAbsolutePath()));
}
return workDir;
}
private static class ContentModifications {
private final Map<Integer, ContentModification> modifications;
ContentModifications(Collection<ContentModification> mods) {
modifications = new HashMap<Integer, ContentModification>(mods.size());
for (ContentModification mod : mods) {
modifications.put(getKey(mod.getItem()), mod);
}
}
ContentModification remove(ContentItem item) {
return modifications.remove(getKey(item));
}
Collection<ContentModification> getModifications() {
return modifications.values();
}
private Integer getKey(ContentItem item) {
final int prime = 31;
int result = 1;
result = prime * result + item.getContentType().hashCode();
result = prime * result + item.getRelativePath().hashCode();
return result;
}
}
/* private static void ls(final File f) {
System.out.println(f.getAbsolutePath());
for(File c : f.listFiles()) {
ls(c, " ");
}
}
private static void ls(final File f, String offset) {
System.out.println(offset + f.getName());
if(f.isDirectory()) {
for(File c : f.listFiles()) {
ls(c, offset + " ");
}
}
}
*/}