001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018: package org.apache.tools.ant.taskdefs;
019:
020: import java.security.DigestInputStream;
021: import java.security.MessageDigest;
022: import java.security.NoSuchAlgorithmException;
023: import java.security.NoSuchProviderException;
024: import java.io.File;
025: import java.io.FileOutputStream;
026: import java.io.FileInputStream;
027: import java.io.FileReader;
028: import java.io.BufferedReader;
029: import java.io.IOException;
030: import java.util.HashMap;
031: import java.util.Map;
032: import java.util.Iterator;
033: import java.util.Hashtable;
034: import java.util.Enumeration;
035: import java.util.Set;
036: import java.util.Arrays;
037: import java.text.MessageFormat;
038: import java.text.ParseException;
039:
040: import org.apache.tools.ant.BuildException;
041: import org.apache.tools.ant.Project;
042: import org.apache.tools.ant.taskdefs.condition.Condition;
043: import org.apache.tools.ant.types.EnumeratedAttribute;
044: import org.apache.tools.ant.types.FileSet;
045: import org.apache.tools.ant.types.ResourceCollection;
046: import org.apache.tools.ant.types.resources.Union;
047: import org.apache.tools.ant.types.resources.Restrict;
048: import org.apache.tools.ant.types.resources.FileResource;
049: import org.apache.tools.ant.types.resources.selectors.Type;
050: import org.apache.tools.ant.util.FileUtils;
051: import org.apache.tools.ant.util.StringUtils;
052:
053: /**
054: * Used to create or verify file checksums.
055: *
056: * @since Ant 1.5
057: *
058: * @ant.task category="control"
059: */
060: public class Checksum extends MatchingTask implements Condition {
061: private static class FileUnion extends Restrict {
062: private Union u;
063:
064: FileUnion() {
065: u = new Union();
066: super .add(u);
067: super .add(Type.FILE);
068: }
069:
070: public void add(ResourceCollection rc) {
071: u.add(rc);
072: }
073: }
074:
075: /**
076: * File for which checksum is to be calculated.
077: */
078: private File file = null;
079:
080: /**
081: * Root directory in which the checksum files will be written.
082: * If not specified, the checksum files will be written
083: * in the same directory as each file.
084: */
085: private File todir;
086:
087: /**
088: * MessageDigest algorithm to be used.
089: */
090: private String algorithm = "MD5";
091: /**
092: * MessageDigest Algorithm provider
093: */
094: private String provider = null;
095: /**
096: * File Extension that is be to used to create or identify
097: * destination file
098: */
099: private String fileext;
100: /**
101: * Holds generated checksum and gets set as a Project Property.
102: */
103: private String property;
104: /**
105: * Holds checksums for all files (both calculated and cached on disk).
106: * Key: java.util.File (source file)
107: * Value: java.lang.String (digest)
108: */
109: private Map allDigests = new HashMap();
110: /**
111: * Holds relative file names for all files (always with a forward slash).
112: * This is used to calculate the total hash.
113: * Key: java.util.File (source file)
114: * Value: java.lang.String (relative file name)
115: */
116: private Map relativeFilePaths = new HashMap();
117: /**
118: * Property where totalChecksum gets set.
119: */
120: private String totalproperty;
121: /**
122: * Whether or not to create a new file.
123: * Defaults to <code>false</code>.
124: */
125: private boolean forceOverwrite;
126: /**
127: * Contains the result of a checksum verification. ("true" or "false")
128: */
129: private String verifyProperty;
130: /**
131: * Resource Collection.
132: */
133: private FileUnion resources = null;
134: /**
135: * Stores SourceFile, DestFile pairs and SourceFile, Property String pairs.
136: */
137: private Hashtable includeFileMap = new Hashtable();
138: /**
139: * Message Digest instance
140: */
141: private MessageDigest messageDigest;
142: /**
143: * is this task being used as a nested condition element?
144: */
145: private boolean isCondition;
146: /**
147: * Size of the read buffer to use.
148: */
149: private int readBufferSize = 8 * 1024;
150:
151: /**
152: * Formater for the checksum file.
153: */
154: private MessageFormat format = FormatElement.getDefault()
155: .getFormat();
156:
157: /**
158: * Sets the file for which the checksum is to be calculated.
159: * @param file a <code>File</code> value
160: */
161: public void setFile(File file) {
162: this .file = file;
163: }
164:
165: /**
166: * Sets the root directory where checksum files will be
167: * written/read
168: * @param todir the directory to write to
169: * @since Ant 1.6
170: */
171: public void setTodir(File todir) {
172: this .todir = todir;
173: }
174:
175: /**
176: * Specifies the algorithm to be used to compute the checksum.
177: * Defaults to "MD5". Other popular algorithms like "SHA" may be used as well.
178: * @param algorithm a <code>String</code> value
179: */
180: public void setAlgorithm(String algorithm) {
181: this .algorithm = algorithm;
182: }
183:
184: /**
185: * Sets the MessageDigest algorithm provider to be used
186: * to calculate the checksum.
187: * @param provider a <code>String</code> value
188: */
189: public void setProvider(String provider) {
190: this .provider = provider;
191: }
192:
193: /**
194: * Sets the file extension that is be to used to
195: * create or identify destination file.
196: * @param fileext a <code>String</code> value
197: */
198: public void setFileext(String fileext) {
199: this .fileext = fileext;
200: }
201:
202: /**
203: * Sets the property to hold the generated checksum.
204: * @param property a <code>String</code> value
205: */
206: public void setProperty(String property) {
207: this .property = property;
208: }
209:
210: /**
211: * Sets the property to hold the generated total checksum
212: * for all files.
213: * @param totalproperty a <code>String</code> value
214: *
215: * @since Ant 1.6
216: */
217: public void setTotalproperty(String totalproperty) {
218: this .totalproperty = totalproperty;
219: }
220:
221: /**
222: * Sets the verify property. This project property holds
223: * the result of a checksum verification - "true" or "false"
224: * @param verifyProperty a <code>String</code> value
225: */
226: public void setVerifyproperty(String verifyProperty) {
227: this .verifyProperty = verifyProperty;
228: }
229:
230: /**
231: * Whether or not to overwrite existing file irrespective of
232: * whether it is newer than
233: * the source file. Defaults to false.
234: * @param forceOverwrite a <code>boolean</code> value
235: */
236: public void setForceOverwrite(boolean forceOverwrite) {
237: this .forceOverwrite = forceOverwrite;
238: }
239:
240: /**
241: * The size of the read buffer to use.
242: * @param size an <code>int</code> value
243: */
244: public void setReadBufferSize(int size) {
245: this .readBufferSize = size;
246: }
247:
248: /**
249: * Select the in/output pattern via a well know format name.
250: * @param e an <code>enumerated</code> value
251: *
252: * @since 1.7.0
253: */
254: public void setFormat(FormatElement e) {
255: format = e.getFormat();
256: }
257:
258: /**
259: * Specify the pattern to use as a MessageFormat pattern.
260: *
261: * <p>{0} gets replaced by the checksum, {1} by the filename.</p>
262: * @param p a <code>String</code> value
263: *
264: * @since 1.7.0
265: */
266: public void setPattern(String p) {
267: format = new MessageFormat(p);
268: }
269:
270: /**
271: * Files to generate checksums for.
272: * @param set a fileset of files to generate checksums for.
273: */
274: public void addFileset(FileSet set) {
275: add(set);
276: }
277:
278: /**
279: * Add a resource collection.
280: * @param rc the ResourceCollection to add.
281: */
282: public void add(ResourceCollection rc) {
283: if (rc == null) {
284: return;
285: }
286: resources = (resources == null) ? new FileUnion() : resources;
287: resources.add(rc);
288: }
289:
290: /**
291: * Calculate the checksum(s).
292: * @throws BuildException on error
293: */
294: public void execute() throws BuildException {
295: isCondition = false;
296: boolean value = validateAndExecute();
297: if (verifyProperty != null) {
298: getProject().setNewProperty(
299: verifyProperty,
300: (value ? Boolean.TRUE.toString() : Boolean.FALSE
301: .toString()));
302: }
303: }
304:
305: /**
306: * Calculate the checksum(s)
307: *
308: * @return Returns true if the checksum verification test passed,
309: * false otherwise.
310: * @throws BuildException on error
311: */
312: public boolean eval() throws BuildException {
313: isCondition = true;
314: return validateAndExecute();
315: }
316:
317: /**
318: * Validate attributes and get down to business.
319: */
320: private boolean validateAndExecute() throws BuildException {
321: String savedFileExt = fileext;
322:
323: if (file == null
324: && (resources == null || resources.size() == 0)) {
325: throw new BuildException(
326: "Specify at least one source - a file or a resource collection.");
327: }
328: if (!(resources == null || resources.isFilesystemOnly())) {
329: throw new BuildException(
330: "Can only calculate checksums for file-based resources.");
331: }
332: if (file != null && file.exists() && file.isDirectory()) {
333: throw new BuildException(
334: "Checksum cannot be generated for directories");
335: }
336: if (file != null && totalproperty != null) {
337: throw new BuildException(
338: "File and Totalproperty cannot co-exist.");
339: }
340: if (property != null && fileext != null) {
341: throw new BuildException(
342: "Property and FileExt cannot co-exist.");
343: }
344: if (property != null) {
345: if (forceOverwrite) {
346: throw new BuildException(
347: "ForceOverwrite cannot be used when Property is specified");
348: }
349: int ct = 0;
350: if (resources != null) {
351: ct += resources.size();
352: }
353: if (file != null) {
354: ct++;
355: }
356: if (ct > 1) {
357: throw new BuildException(
358: "Multiple files cannot be used when Property is specified");
359: }
360: }
361: if (verifyProperty != null) {
362: isCondition = true;
363: }
364: if (verifyProperty != null && forceOverwrite) {
365: throw new BuildException(
366: "VerifyProperty and ForceOverwrite cannot co-exist.");
367: }
368: if (isCondition && forceOverwrite) {
369: throw new BuildException(
370: "ForceOverwrite cannot be used when conditions are being used.");
371: }
372: messageDigest = null;
373: if (provider != null) {
374: try {
375: messageDigest = MessageDigest.getInstance(algorithm,
376: provider);
377: } catch (NoSuchAlgorithmException noalgo) {
378: throw new BuildException(noalgo, getLocation());
379: } catch (NoSuchProviderException noprovider) {
380: throw new BuildException(noprovider, getLocation());
381: }
382: } else {
383: try {
384: messageDigest = MessageDigest.getInstance(algorithm);
385: } catch (NoSuchAlgorithmException noalgo) {
386: throw new BuildException(noalgo, getLocation());
387: }
388: }
389: if (messageDigest == null) {
390: throw new BuildException("Unable to create Message Digest",
391: getLocation());
392: }
393: if (fileext == null) {
394: fileext = "." + algorithm;
395: } else if (fileext.trim().length() == 0) {
396: throw new BuildException(
397: "File extension when specified must not be an empty string");
398: }
399: try {
400: if (resources != null) {
401: for (Iterator i = resources.iterator(); i.hasNext();) {
402: FileResource fr = (FileResource) i.next();
403: File src = fr.getFile();
404: if (totalproperty != null || todir != null) {
405: // Use '/' to calculate digest based on file name.
406: // This is required in order to get the same result
407: // on different platforms.
408: relativeFilePaths.put(src, fr.getName()
409: .replace(File.separatorChar, '/'));
410: }
411: addToIncludeFileMap(src);
412: }
413: }
414: if (file != null) {
415: if (totalproperty != null || todir != null) {
416: relativeFilePaths.put(file, file.getName().replace(
417: File.separatorChar, '/'));
418: }
419: addToIncludeFileMap(file);
420: }
421: return generateChecksums();
422: } finally {
423: fileext = savedFileExt;
424: includeFileMap.clear();
425: }
426: }
427:
428: /**
429: * Add key-value pair to the hashtable upon which
430: * to later operate upon.
431: */
432: private void addToIncludeFileMap(File file) throws BuildException {
433: if (file.exists()) {
434: if (property == null) {
435: File checksumFile = getChecksumFile(file);
436: if (forceOverwrite
437: || isCondition
438: || (file.lastModified() > checksumFile
439: .lastModified())) {
440: includeFileMap.put(file, checksumFile);
441: } else {
442: log(file + " omitted as " + checksumFile
443: + " is up to date.", Project.MSG_VERBOSE);
444: if (totalproperty != null) {
445: // Read the checksum from disk.
446: String checksum = readChecksum(checksumFile);
447: byte[] digest = decodeHex(checksum
448: .toCharArray());
449: allDigests.put(file, digest);
450: }
451: }
452: } else {
453: includeFileMap.put(file, property);
454: }
455: } else {
456: String message = "Could not find file "
457: + file.getAbsolutePath()
458: + " to generate checksum for.";
459: log(message);
460: throw new BuildException(message, getLocation());
461: }
462: }
463:
464: private File getChecksumFile(File file) {
465: File directory;
466: if (todir != null) {
467: // A separate directory was explicitly declared
468: String path = (String) relativeFilePaths.get(file);
469: if (path == null) {
470: //bug 37386. this should not occur, but it has, once.
471: throw new BuildException("Internal error: "
472: + "relativeFilePaths could not match file"
473: + file + "\n"
474: + "please file a bug report on this");
475: }
476: directory = new File(todir, path).getParentFile();
477: // Create the directory, as it might not exist.
478: directory.mkdirs();
479: } else {
480: // Just use the same directory as the file itself.
481: // This directory will exist
482: directory = file.getParentFile();
483: }
484: File checksumFile = new File(directory, file.getName()
485: + fileext);
486: return checksumFile;
487: }
488:
489: /**
490: * Generate checksum(s) using the message digest created earlier.
491: */
492: private boolean generateChecksums() throws BuildException {
493: boolean checksumMatches = true;
494: FileInputStream fis = null;
495: FileOutputStream fos = null;
496: byte[] buf = new byte[readBufferSize];
497: try {
498: for (Enumeration e = includeFileMap.keys(); e
499: .hasMoreElements();) {
500: messageDigest.reset();
501: File src = (File) e.nextElement();
502: if (!isCondition) {
503: log("Calculating " + algorithm + " checksum for "
504: + src, Project.MSG_VERBOSE);
505: }
506: fis = new FileInputStream(src);
507: DigestInputStream dis = new DigestInputStream(fis,
508: messageDigest);
509: while (dis.read(buf, 0, readBufferSize) != -1) {
510: // Empty statement
511: }
512: dis.close();
513: fis.close();
514: fis = null;
515: byte[] fileDigest = messageDigest.digest();
516: if (totalproperty != null) {
517: allDigests.put(src, fileDigest);
518: }
519: String checksum = createDigestString(fileDigest);
520: //can either be a property name string or a file
521: Object destination = includeFileMap.get(src);
522: if (destination instanceof java.lang.String) {
523: String prop = (String) destination;
524: if (isCondition) {
525: checksumMatches = checksumMatches
526: && checksum.equals(property);
527: } else {
528: getProject().setNewProperty(prop, checksum);
529: }
530: } else if (destination instanceof java.io.File) {
531: if (isCondition) {
532: File existingFile = (File) destination;
533: if (existingFile.exists()) {
534: try {
535: String suppliedChecksum = readChecksum(existingFile);
536: checksumMatches = checksumMatches
537: && checksum
538: .equals(suppliedChecksum);
539: } catch (BuildException be) {
540: // file is on wrong format, swallow
541: checksumMatches = false;
542: }
543: } else {
544: checksumMatches = false;
545: }
546: } else {
547: File dest = (File) destination;
548: fos = new FileOutputStream(dest);
549: fos.write(format
550: .format(
551: new Object[] { checksum,
552: src.getName(), })
553: .getBytes());
554: fos.write(StringUtils.LINE_SEP.getBytes());
555: fos.close();
556: fos = null;
557: }
558: }
559: }
560: if (totalproperty != null) {
561: // Calculate the total checksum
562: // Convert the keys (source files) into a sorted array.
563: Set keys = allDigests.keySet();
564: Object[] keyArray = keys.toArray();
565: // File is Comparable, so sorting is trivial
566: Arrays.sort(keyArray);
567: // Loop over the checksums and generate a total hash.
568: messageDigest.reset();
569: for (int i = 0; i < keyArray.length; i++) {
570: File src = (File) keyArray[i];
571:
572: // Add the digest for the file content
573: byte[] digest = (byte[]) allDigests.get(src);
574: messageDigest.update(digest);
575:
576: // Add the file path
577: String fileName = (String) relativeFilePaths
578: .get(src);
579: messageDigest.update(fileName.getBytes());
580: }
581: String totalChecksum = createDigestString(messageDigest
582: .digest());
583: getProject().setNewProperty(totalproperty,
584: totalChecksum);
585: }
586: } catch (Exception e) {
587: throw new BuildException(e, getLocation());
588: } finally {
589: FileUtils.close(fis);
590: FileUtils.close(fos);
591: }
592: return checksumMatches;
593: }
594:
595: private String createDigestString(byte[] fileDigest) {
596: StringBuffer checksumSb = new StringBuffer();
597: for (int i = 0; i < fileDigest.length; i++) {
598: String hexStr = Integer.toHexString(0x00ff & fileDigest[i]);
599: if (hexStr.length() < 2) {
600: checksumSb.append("0");
601: }
602: checksumSb.append(hexStr);
603: }
604: return checksumSb.toString();
605: }
606:
607: /**
608: * Converts an array of characters representing hexadecimal values into an
609: * array of bytes of those same values. The returned array will be half the
610: * length of the passed array, as it takes two characters to represent any
611: * given byte. An exception is thrown if the passed char array has an odd
612: * number of elements.
613: *
614: * NOTE: This code is copied from jakarta-commons codec.
615: * @param data an array of characters representing hexadecimal values
616: * @return the converted array of bytes
617: * @throws BuildException on error
618: */
619: public static byte[] decodeHex(char[] data) throws BuildException {
620: int l = data.length;
621:
622: if ((l & 0x01) != 0) {
623: throw new BuildException("odd number of characters.");
624: }
625:
626: byte[] out = new byte[l >> 1];
627:
628: // two characters form the hex value.
629: for (int i = 0, j = 0; j < l; i++) {
630: int f = Character.digit(data[j++], 16) << 4;
631: f = f | Character.digit(data[j++], 16);
632: out[i] = (byte) (f & 0xFF);
633: }
634:
635: return out;
636: }
637:
638: /**
639: * reads the checksum from a file using the specified format.
640: *
641: * @since 1.7
642: */
643: private String readChecksum(File f) {
644: BufferedReader diskChecksumReader = null;
645: try {
646: diskChecksumReader = new BufferedReader(new FileReader(f));
647: Object[] result = format.parse(diskChecksumReader
648: .readLine());
649: if (result == null || result.length == 0
650: || result[0] == null) {
651: throw new BuildException("failed to find a checksum");
652: }
653: return (String) result[0];
654: } catch (IOException e) {
655: throw new BuildException(
656: "Couldn't read checksum file " + f, e);
657: } catch (ParseException e) {
658: throw new BuildException(
659: "Couldn't read checksum file " + f, e);
660: } finally {
661: FileUtils.close(diskChecksumReader);
662: }
663: }
664:
665: /**
666: * Helper class for the format attribute.
667: *
668: * @since 1.7
669: */
670: public static class FormatElement extends EnumeratedAttribute {
671: private static HashMap formatMap = new HashMap();
672: private static final String CHECKSUM = "CHECKSUM";
673: private static final String MD5SUM = "MD5SUM";
674: private static final String SVF = "SVF";
675:
676: static {
677: formatMap.put(CHECKSUM, new MessageFormat("{0}"));
678: formatMap.put(MD5SUM, new MessageFormat("{0} *{1}"));
679: formatMap.put(SVF, new MessageFormat("MD5 ({1}) = {0}"));
680: }
681:
682: /** Constructor for FormatElement */
683: public FormatElement() {
684: super ();
685: }
686:
687: /**
688: * Get the default value - CHECKSUM.
689: * @return the defaul value.
690: */
691: public static FormatElement getDefault() {
692: FormatElement e = new FormatElement();
693: e.setValue(CHECKSUM);
694: return e;
695: }
696:
697: /**
698: * Convert this enumerated type to a <code>MessageFormat</code>.
699: * @return a <code>MessageFormat</code> object.
700: */
701: public MessageFormat getFormat() {
702: return (MessageFormat) formatMap.get(getValue());
703: }
704:
705: /**
706: * Get the valid values.
707: * @return an array of values.
708: */
709: public String[] getValues() {
710: return new String[] { CHECKSUM, MD5SUM, SVF };
711: }
712: }
713: }
|