/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.agent.update;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
/**
* Compares two files, an old one and a new one and if they differ, one will win and be kept as-is
* in the new file's parent directory while the other one will get copied under a new name in
* the new file's parent directory as a backup copy.
* If the two files are identical, this task is a no-op and does nothing.
* If backupextension is not specified, the file that is not to be kept will not be backed up.
*
* <copy-with-backup olddir="old" newdir="new" filename="file" keep="new" backupextension=".default" />
*
* That will compare old/file to new/file and if they are different, the new one will be kept
* as is in directory new and the old one will be copied into directory
* 'new' but with a name of "file" appended with the backup extension ".default".
*
* The "old" file is always copied to the directory where the "new" file is located; the "keep" attribute
* determines which file gets renamed with the backup extension and which keeps its file name intact.
*
* This allows us to tell which customized files from an old install should override the new install's
* default files and which of the new default files should override any previous customizations made
* to an old install.
*
* This loads in the content of both files to generate the MD5 - do not use this task on large binary
* files. If we need to do this kind of task on files other than small text files, we need to refactor
* the way the MD5's are calculated.
*
* @author John Mazzitelli
*/
public class CopyWithBackup extends Task {
private File oldDirectory;
private File newDirectory;
private String filename;
private String keep; // must be either "old" or "new"
private String backupextension;
private Boolean failonerror = Boolean.FALSE;
public void setOlddir(File dir) {
this.oldDirectory = dir;
}
public void setNewdir(File dir) {
this.newDirectory = dir;
}
public void setFilename(String filename) {
this.filename = filename;
}
public void setKeep(String keep) {
this.keep = keep;
}
public void setBackupextension(String ext) {
this.backupextension = ext;
}
public void setFailonerror(Boolean flag) {
this.failonerror = flag;
}
/**
* @see org.apache.tools.ant.Task#execute()
*/
@Override
public void execute() throws BuildException {
validateAttributes();
try {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new BuildException(e); // should never occur; this would be bad - JRE has this builtin
}
File oldFile = new File(oldDirectory, filename);
File newFile = new File(newDirectory, filename);
log("* old file=" + oldFile, Project.MSG_DEBUG);
log("* old file exists=" + oldFile.exists(), Project.MSG_DEBUG);
log("* new file=" + newFile, Project.MSG_DEBUG);
log("* new file exists=" + newFile.exists(), Project.MSG_DEBUG);
log("* backup extension=" + backupextension, Project.MSG_DEBUG);
log("* keep=" + keep, Project.MSG_DEBUG);
// handle the special cases when one or both do not exist
if (!oldFile.exists()) {
if (!newFile.exists()) {
log("old file and new file do not exist - nothing to be done");
return;
}
if (keep.equals("new")) {
log("old file does not exist, new file does, keeping new file as-is");
} else {
if (backupextension == null) {
log("old file does not exist, new file does, removing new file");
if (!newFile.delete()) {
throw new BuildException("Cannot remove new file [" + newFile + "]");
}
} else {
log("old file does not exist, new file does, backing up new file");
if (!newFile.renameTo(new File(newDirectory, filename + backupextension))) {
throw new BuildException("Cannot backup new file [" + newFile + "]");
}
}
}
} else if (!newFile.exists()) {
if (keep.equals("new")) {
if (backupextension == null) {
log("old file exists, new file does not, do nothing");
} else {
log("old file exists, new file does not, backup old");
copy(oldFile, new File(newDirectory, filename + backupextension));
}
} else {
log("old file exists, new file does not, copying old file");
copy(oldFile, newFile); // doesn't matter what backupextension is, there is no new file to care
}
} else {
// both files exist - we need to compare them and do our thing if they are different
byte[] oldMD5Bytes = messageDigest.digest(slurp(oldFile));
messageDigest.reset();
byte[] newMD5Bytes = messageDigest.digest(slurp(newFile));
if (MessageDigest.isEqual(oldMD5Bytes, newMD5Bytes)) {
log("old file and new file are the same, nothing to do");
return;
}
if (keep.equals("new")) {
if (backupextension == null) {
log("files differ, keeping new, not backing up old");
} else {
log("files differ, keeping new, backing up old");
copy(oldFile, new File(newDirectory, filename + backupextension));
}
} else {
if (backupextension == null) {
log("files differ, keeping old, not backing up new");
newFile.delete();
copy(oldFile, newFile);
} else {
log("files differ, keeping old, backing up new");
newFile.renameTo(new File(newDirectory, filename + backupextension));
copy(oldFile, newFile);
}
}
}
} catch (BuildException e) {
if (failonerror.booleanValue()) {
throw e;
} else {
log("got a failure but will not exit: " + e);
}
}
return;
}
private byte[] slurp(File file) throws BuildException {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
copy(new FileInputStream(file), out);
return out.toByteArray();
} catch (Exception e) {
throw new BuildException("Stream data cannot be slurped", e);
}
}
private void copy(File src, File dest) throws BuildException {
if (dest.exists()) {
throw new BuildException("Cannot copy [" + src + "] to [" + dest + "] because the latter exists");
}
try {
copy(new FileInputStream(src), new FileOutputStream(dest));
} catch (Exception e) {
throw new BuildException("Cannot copy [" + src + "] to [" + dest + "]", e);
}
}
private long copy(InputStream input, OutputStream output) throws RuntimeException {
long numBytesCopied = 0;
int bufferSize = 32768;
try {
// make sure we buffer the input
input = new BufferedInputStream(input, bufferSize);
byte[] buffer = new byte[bufferSize];
for (int bytesRead = input.read(buffer); bytesRead != -1; bytesRead = input.read(buffer)) {
output.write(buffer, 0, bytesRead);
numBytesCopied += bytesRead;
}
output.flush();
} catch (Exception e) {
throw new BuildException("Stream data cannot be copied", e);
} finally {
try {
output.close();
} catch (IOException ioe) {
}
try {
input.close();
} catch (IOException ioe) {
}
}
return numBytesCopied;
}
private void validateAttributes() throws BuildException {
if (filename == null) {
throw new BuildException("Must specify 'filename'");
}
if (oldDirectory == null) {
throw new BuildException("Must specify 'olddir' directory");
}
if (newDirectory == null) {
throw new BuildException("Must specify 'newdir' directory");
} else if (!newDirectory.exists()) {
throw new BuildException("'newdir' directory must exist: " + newDirectory);
} else if (!newDirectory.isDirectory()) {
throw new BuildException("'newdir' must be a directory: " + newDirectory);
}
if (keep == null) {
throw new BuildException("Must specify 'keep' as either 'old' or 'new'");
} else if (!"old".equals(keep) && !"new".equals(keep)) {
throw new BuildException("'keep' must be one of: [old, new]");
}
// an empty extension is the same as not specifying it at all
if (backupextension != null && backupextension.trim().length() == 0) {
backupextension = null;
}
}
}