@@ -53,19 +53,25 @@
*/
package org.apache.tools.ant.taskdefs;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import java.util.Hashtable;
import java.util.Enumeration;
import java.util.Set;
import java.util.Arrays;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
@@ -76,16 +82,26 @@ import org.apache.tools.ant.types.FileSet;
* Used to create or verify file checksums.
*
* @author Magesh Umasankar
* @author Aslak Hellesoy
*
* @since Ant 1.5
*
* @ant.task category="control"
*/
public class Checksum extends MatchingTask implements Condition {
/**
* File for which checksum is to be calculated.
*/
private File file = null;
/**
* Root directory in which the checksu files will be written.
* If not specified, the checksum files will be written
* in the same directory as each file.
*/
private File todir;
/**
* MessageDigest algorithm to be used.
*/
@@ -103,6 +119,23 @@ public class Checksum extends MatchingTask implements Condition {
* Holds generated checksum and gets set as a Project Property.
*/
private String property;
/**
* Holds checksums for all files (both calculated and cached on disk).
* Key: java.util.File (source file)
* Value: java.lang.String (digest)
*/
private Map allDigests = new HashMap();
/**
* Holds relative file names for all files (always with a forward slash).
* This is used to calculate the total hash.
* Key: java.util.File (source file)
* Value: java.lang.String (relative file name)
*/
private Map relativeFilePaths = new HashMap();
/**
* Property where totalChecksum gets set.
*/
private String totalproperty;
/**
* Whether or not to create a new file.
* Defaults to <code>false</code>.
@@ -140,6 +173,14 @@ public class Checksum extends MatchingTask implements Condition {
this.file = file;
}
/**
* Sets the root directory where checksum files will be
* written/read
*/
public void setTodir(File todir) {
this.todir = todir;
}
/**
* Specifies the algorithm to be used to compute the checksum.
* Defaults to "MD5". Other popular algorithms like "SHA" may be used as well.
@@ -171,6 +212,14 @@ public class Checksum extends MatchingTask implements Condition {
this.property = property;
}
/**
* Sets the property to hold the generated total checksum
* for all files.
*/
public void setTotalproperty(String totalproperty) {
this.totalproperty = totalproperty;
}
/**
* Sets the verify property. This project property holds
* the result of a checksum verification - "true" or "false"
@@ -241,6 +290,11 @@ public class Checksum extends MatchingTask implements Condition {
"Checksum cannot be generated for directories");
}
if (file != null && totalproperty != null) {
throw new BuildException(
"File and Totalproperty cannot co-exist.");
}
if (property != null && fileext != null) {
throw new BuildException(
"Property and FileExt cannot co-exist.");
@@ -309,8 +363,6 @@ public class Checksum extends MatchingTask implements Condition {
}
try {
addToIncludeFileMap(file);
int sizeofFileSet = filesets.size();
for (int i = 0; i < sizeofFileSet; i++) {
FileSet fs = (FileSet) filesets.elementAt(i);
@@ -318,10 +370,19 @@ public class Checksum extends MatchingTask implements Condition {
String[] srcFiles = ds.getIncludedFiles();
for (int j = 0; j < srcFiles.length; j++) {
File src = new File(fs.getDir(getProject()), srcFiles[j]);
if (totalproperty != null) {
// Use '/' to calculate digest based on file name.
// This is required in order to get the same result
// on different platforms.
String relativePath = srcFiles[j].replace(File.separatorChar, '/');
relativeFilePaths.put(src, relativePath);
}
addToIncludeFileMap(src);
}
}
addToIncludeFileMap(file);
return generateChecksums();
} finally {
fileext = savedFileExt;
@@ -337,14 +398,25 @@ public class Checksum extends MatchingTask implements Condition {
if (file != null) {
if (file.exists()) {
if (property == null) {
File dest
= new File(file.getParent(), file.getName() + fileext);
File checksumFile = getChecksumFile(file);
if (forceOverwrite || isCondition ||
(file.lastModified() > dest .lastModified())) {
includeFileMap.put(file, dest );
(file.lastModified() > checksumFile .lastModified())) {
includeFileMap.put(file, checksumFile );
} else {
log(file + " omitted as " + dest + " is up to date.",
log(file + " omitted as " + checksumFile + " is up to date.",
Project.MSG_VERBOSE);
if (totalproperty != null) {
// Read the checksum from disk.
String checksum = null;
try {
BufferedReader diskChecksumReader = new BufferedReader(new FileReader(checksumFile));
checksum = diskChecksumReader.readLine();
} catch (IOException e) {
throw new BuildException("Couldn't read checksum file " + checksumFile, e);
}
byte[] digest = decodeHex(checksum.toCharArray());
allDigests.put(file,digest );
}
}
} else {
includeFileMap.put(file, property);
@@ -359,6 +431,23 @@ public class Checksum extends MatchingTask implements Condition {
}
}
private File getChecksumFile(File file) {
File directory;
if (todir != null) {
// A separate directory was explicitly declared
String path = (String) relativeFilePaths.get(file);
directory = new File(todir, path).getParentFile();
// Create the directory, as it might not exist.
directory.mkdirs();
} else {
// Just use the same directory as the file itself.
// This directory will exist
directory = file.getParentFile();
}
File checksumFile = new File(directory, file.getName() + fileext);
return checksumFile;
}
/**
* Generate checksum(s) using the message digest created earlier.
*/
@@ -384,15 +473,10 @@ public class Checksum extends MatchingTask implements Condition {
fis.close();
fis = null;
byte[] fileDigest = messageDigest.digest ();
StringBuffer checksumSb = new StringBuffer();
for (int i = 0; i < fileDigest.length; i++) {
String hexStr = Integer.toHexString(0x00ff & fileDigest[i]);
if (hexStr.length() < 2) {
checksumSb.append("0");
}
checksumSb.append(hexStr);
if (totalproperty != null) {
allDigests.put(src,fileDigest);
}
String checksum = checksumSb.toString( );
String checksum = createDigestString(fileDigest);
//can either be a property name string or a file
Object destination = includeFileMap.get(src);
if (destination instanceof java.lang.String) {
@@ -429,6 +513,29 @@ public class Checksum extends MatchingTask implements Condition {
}
}
}
if (totalproperty != null) {
// Calculate the total checksum
// Convert the keys (source files) into a sorted array.
Set keys = allDigests.keySet();
Object[] keyArray = keys.toArray();
// File is Comparable, so sorting is trivial
Arrays.sort(keyArray);
// Loop over the checksums and generate a total hash.
messageDigest.reset();
for (int i = 0; i < keyArray.length; i++) {
File src = (File) keyArray[i];
// Add the digest for the file content
byte[] digest = (byte[]) allDigests.get(src);
messageDigest.update(digest);
// Add the file path
String fileName = (String) relativeFilePaths.get(src);
messageDigest.update(fileName.getBytes());
}
String totalChecksum = createDigestString(messageDigest.digest());
getProject().setNewProperty(totalproperty, totalChecksum);
}
} catch (Exception e) {
throw new BuildException(e, getLocation());
} finally {
@@ -445,4 +552,44 @@ public class Checksum extends MatchingTask implements Condition {
}
return checksumMatches;
}
private String createDigestString(byte[] fileDigest) {
StringBuffer checksumSb = new StringBuffer();
for (int i = 0; i < fileDigest.length; i++) {
String hexStr = Integer.toHexString(0x00ff & fileDigest[i]);
if (hexStr.length() < 2) {
checksumSb.append("0");
}
checksumSb.append(hexStr);
}
return checksumSb.toString();
}
/**
* Converts an array of characters representing hexidecimal values into an
* array of bytes of those same values. The returned array will be half the
* length of the passed array, as it takes two characters to represent any
* given byte. An exception is thrown if the passed char array has an odd
* number of elements.
*
* NOTE: This code is copied from jakarta-commons codec.
*/
public static byte[] decodeHex(char[] data) throws BuildException {
int l = data.length;
if ((l & 0x01) != 0) {
throw new BuildException("odd number of characters.");
}
byte[] out = new byte[l >> 1];
// two characters form the hex value.
for (int i = 0, j = 0; j < l; i++) {
int f = Character.digit(data[j++], 16) << 4;
f = f | Character.digit(data[j++], 16);
out[i] = (byte) (f & 0xFF);
}
return out;
}
}