View Javadoc

1   package org.sf.jlaunchpad.util;
2   
3   import java.security.DigestInputStream;
4   import java.security.MessageDigest;
5   import java.security.NoSuchAlgorithmException;
6   import java.security.NoSuchProviderException;
7   import java.io.*;
8   import java.util.*;
9   import java.text.MessageFormat;
10  import java.text.ParseException;
11  
12  /**
13   * Used to create or verify file checksums.
14   *
15   * @ant.task category="control"
16   * @since Ant 1.5
17   */
18  public class Checksum {
19  
20    /**
21     * File for which checksum is to be calculated.
22     */
23    private File file = null;
24  
25    /**
26     * Root directory in which the checksum files will be written.
27     * If not specified, the checksum files will be written
28     * in the same directory as each file.
29     */
30    private File todir;
31  
32    /**
33     * MessageDigest algorithm to be used.
34     */
35    private String algorithm = "MD5";
36    /**
37     * MessageDigest Algorithm provider
38     */
39    private String provider = null;
40    /**
41     * File Extension that is be to used to create or identify
42     * destination file
43     */
44    private String fileext;
45    /**
46     * Holds generated checksum and gets set as a Project Property.
47     */
48    private String property;
49    /**
50     * Holds checksums for all files (both calculated and cached on disk).
51     * Key:   java.util.File (source file)
52     * Value: java.lang.String (digest)
53     */
54    private Map allDigests = new HashMap();
55    /**
56     * Holds relative file names for all files (always with a forward slash).
57     * This is used to calculate the total hash.
58     * Key:   java.util.File (source file)
59     * Value: java.lang.String (relative file name)
60     */
61    private Map relativeFilePaths = new HashMap();
62    /**
63     * Property where totalChecksum gets set.
64     */
65    private String totalproperty;
66    /**
67     * Whether or not to create a new file.
68     * Defaults to <code>false</code>.
69     */
70    private boolean forceOverwrite;
71    /**
72     * Contains the result of a checksum verification. ("true" or "false")
73     */
74    private String verifyProperty;
75    /**
76     * Resource Collection.
77     */
78    //private FileUnion resources = null;
79    /**
80     * Stores SourceFile, DestFile pairs and SourceFile, Property String pairs.
81     */
82    private Hashtable includeFileMap = new Hashtable();
83    /**
84     * Message Digest instance
85     */
86    private MessageDigest messageDigest;
87    /**
88     * is this task being used as a nested condition element?
89     */
90    private boolean isCondition;
91    /**
92     * Size of the read buffer to use.
93     */
94    private int readBufferSize = 8 * 1024;
95  
96    /**
97     * Formater for the checksum file.
98     */
99    private MessageFormat format = FormatElement.getDefault().getFormat();
100 
101   /**
102    * Sets the file for which the checksum is to be calculated.
103    *
104    * @param file a <code>File</code> value
105    */
106   public void setFile(File file) {
107     this.file = file;
108   }
109 
110   /**
111    * Sets the root directory where checksum files will be
112    * written/read
113    *
114    * @param todir the directory to write to
115    * @since Ant 1.6
116    */
117   public void setTodir(File todir) {
118     this.todir = todir;
119   }
120 
121   /**
122    * Specifies the algorithm to be used to compute the checksum.
123    * Defaults to "MD5". Other popular algorithms like "SHA" may be used as well.
124    *
125    * @param algorithm a <code>String</code> value
126    */
127   public void setAlgorithm(String algorithm) {
128     this.algorithm = algorithm;
129   }
130 
131   /**
132    * Sets the MessageDigest algorithm provider to be used
133    * to calculate the checksum.
134    *
135    * @param provider a <code>String</code> value
136    */
137   public void setProvider(String provider) {
138     this.provider = provider;
139   }
140 
141   /**
142    * Sets the file extension that is be to used to
143    * create or identify destination file.
144    *
145    * @param fileext a <code>String</code> value
146    */
147   public void setFileext(String fileext) {
148     this.fileext = fileext;
149   }
150 
151   /**
152    * Sets the property to hold the generated checksum.
153    *
154    * @param property a <code>String</code> value
155    */
156   public void setProperty(String property) {
157     this.property = property;
158   }
159 
160   /**
161    * Sets the property to hold the generated total checksum
162    * for all files.
163    *
164    * @param totalproperty a <code>String</code> value
165    * @since Ant 1.6
166    */
167   public void setTotalproperty(String totalproperty) {
168     this.totalproperty = totalproperty;
169   }
170 
171   /**
172    * Sets the verify property.  This project property holds
173    * the result of a checksum verification - "true" or "false"
174    *
175    * @param verifyProperty a <code>String</code> value
176    */
177   public void setVerifyproperty(String verifyProperty) {
178     this.verifyProperty = verifyProperty;
179   }
180 
181   /**
182    * Whether or not to overwrite existing file irrespective of
183    * whether it is newer than
184    * the source file.  Defaults to false.
185    *
186    * @param forceOverwrite a <code>boolean</code> value
187    */
188   public void setForceOverwrite(boolean forceOverwrite) {
189     this.forceOverwrite = forceOverwrite;
190   }
191 
192   /**
193    * The size of the read buffer to use.
194    *
195    * @param size an <code>int</code> value
196    */
197   public void setReadBufferSize(int size) {
198     this.readBufferSize = size;
199   }
200 
201   /**
202    * Select the in/output pattern via a well know format name.
203    *
204    * @param e an <code>enumerated</code> value
205    * @since 1.7.0
206    */
207   public void setFormat(FormatElement e) {
208     format = e.getFormat();
209   }
210 
211   /**
212    * Specify the pattern to use as a MessageFormat pattern.
213    * <p/>
214    * <p>{0} gets replaced by the checksum, {1} by the filename.</p>
215    *
216    * @param p a <code>String</code> value
217    * @since 1.7.0
218    */
219   public void setPattern(String p) {
220     format = new MessageFormat(p);
221   }
222 
223   /**
224    * Calculate the checksum(s).
225    *
226    */
227   public void execute() {
228     isCondition = false;
229     boolean value = validateAndExecute();
230 /*    if (verifyProperty != null) {
231       getProject().setNewProperty(
232         verifyProperty,
233         (value ? Boolean.TRUE.toString() : Boolean.FALSE.toString()));
234     }
235 */
236   }
237   
238   /**
239    * Validate attributes and get down to business.
240    */
241   private boolean validateAndExecute() {
242     String savedFileExt = fileext;
243 
244     if (file != null && file.exists() && file.isDirectory()) {
245       throw new RuntimeException("Checksum cannot be generated for directories");
246     }
247     if (file != null && totalproperty != null) {
248       throw new RuntimeException("File and Totalproperty cannot co-exist.");
249     }
250     if (property != null && fileext != null) {
251       throw new RuntimeException("Property and FileExt cannot co-exist.");
252     }
253     if (property != null) {
254       if (forceOverwrite) {
255         throw new RuntimeException(
256           "ForceOverwrite cannot be used when Property is specified");
257       }
258       int ct = 0;
259 
260       if (file != null) {
261         ct++;
262       }
263       if (ct > 1) {
264         throw new RuntimeException(
265           "Multiple files cannot be used when Property is specified");
266       }
267     }
268     if (verifyProperty != null) {
269       isCondition = true;
270     }
271     if (verifyProperty != null && forceOverwrite) {
272       throw new RuntimeException("VerifyProperty and ForceOverwrite cannot co-exist.");
273     }
274     if (isCondition && forceOverwrite) {
275       throw new RuntimeException(
276         "ForceOverwrite cannot be used when conditions are being used.");
277     }
278     messageDigest = null;
279     if (provider != null) {
280       try {
281         messageDigest = MessageDigest.getInstance(algorithm, provider);
282       } catch (NoSuchAlgorithmException noalgo) {
283         throw new RuntimeException(noalgo);
284       } catch (NoSuchProviderException noprovider) {
285         throw new RuntimeException(noprovider);
286       }
287     } else {
288       try {
289         messageDigest = MessageDigest.getInstance(algorithm);
290       } catch (NoSuchAlgorithmException noalgo) {
291         throw new RuntimeException(noalgo);
292       }
293     }
294     if (messageDigest == null) {
295       throw new RuntimeException("Unable to create Message Digest");
296     }
297     if (fileext == null) {
298       fileext = "." + algorithm;
299     } else if (fileext.trim().length() == 0) {
300       throw new RuntimeException("File extension when specified must not be an empty string");
301     }
302     try {
303       if (file != null) {
304         if (totalproperty != null || todir != null) {
305           relativeFilePaths.put(
306             file, file.getName().replace(File.separatorChar, '/'));
307         }
308         addToIncludeFileMap(file);
309       }
310       return generateChecksums();
311     } finally {
312       fileext = savedFileExt;
313       includeFileMap.clear();
314     }
315   }
316 
317   /**
318    * Add key-value pair to the hashtable upon which
319    * to later operate upon.
320    */
321   private void addToIncludeFileMap(File file) throws RuntimeException {
322     if (file.exists()) {
323       if (property == null) {
324         File checksumFile = getChecksumFile(file);
325         if (forceOverwrite || isCondition
326           || (file.lastModified() > checksumFile.lastModified())) {
327           includeFileMap.put(file, checksumFile);
328         } else {
329           //log(file + " omitted as " + checksumFile + " is up to date.",            Project.MSG_VERBOSE);
330           if (totalproperty != null) {
331             // Read the checksum from disk.
332             String checksum = readChecksum(checksumFile);
333             byte[] digest = decodeHex(checksum.toCharArray());
334             allDigests.put(file, digest);
335           }
336         }
337       } else {
338         includeFileMap.put(file, property);
339       }
340     } else {
341       String message = "Could not find file "
342         + file.getAbsolutePath()
343         + " to generate checksum for.";
344       //log(message);
345       throw new RuntimeException(message);
346     }
347   }
348 
349   private File getChecksumFile(File file) {
350     File directory;
351     if (todir != null) {
352       // A separate directory was explicitly declared
353       String path = (String) relativeFilePaths.get(file);
354       if (path == null) {
355         //bug 37386. this should not occur, but it has, once.
356         throw new RuntimeException(
357           "Internal error: "
358             + "relativeFilePaths could not match file"
359             + file + "\n"
360             + "please file a bug report on this");
361       }
362       directory = new File(todir, path).getParentFile();
363       // Create the directory, as it might not exist.
364       directory.mkdirs();
365     } else {
366       // Just use the same directory as the file itself.
367       // This directory will exist
368       directory = file.getParentFile();
369     }
370 
371     return new File(directory, file.getName() + fileext);
372   }
373 
374   /**
375    * Generate checksum(s) using the message digest created earlier.
376    */
377   private boolean generateChecksums() {
378     boolean checksumMatches = true;
379     FileInputStream fis = null;
380     FileOutputStream fos = null;
381     byte[] buf = new byte[readBufferSize];
382     try {
383       for (Enumeration e = includeFileMap.keys(); e.hasMoreElements();) {
384         messageDigest.reset();
385         File src = (File) e.nextElement();
386         if (!isCondition) {
387           //log("Calculating " + algorithm + " checksum for " + src, Project.MSG_VERBOSE);
388         }
389         fis = new FileInputStream(src);
390         DigestInputStream dis = new DigestInputStream(fis,
391           messageDigest);
392         while (dis.read(buf, 0, readBufferSize) != -1) {
393           // Empty statement
394         }
395         dis.close();
396         fis.close();
397         fis = null;
398         byte[] fileDigest = messageDigest.digest();
399         if (totalproperty != null) {
400           allDigests.put(src, fileDigest);
401         }
402         String checksum = createDigestString(fileDigest);
403         //can either be a property name string or a file
404         Object destination = includeFileMap.get(src);
405         if (destination instanceof java.lang.String) {
406           String prop = (String) destination;
407           if (isCondition) {
408             checksumMatches
409               = checksumMatches && checksum.equals(property);
410           } else {
411             //getProject().setNewProperty(prop, checksum);
412           }
413         } else if (destination instanceof java.io.File) {
414           if (isCondition) {
415             File existingFile = (File) destination;
416             if (existingFile.exists()) {
417               try {
418                 String suppliedChecksum =
419                   readChecksum(existingFile);
420                 checksumMatches = checksumMatches
421                   && checksum.equals(suppliedChecksum);
422               } catch (Exception be) {
423                 // file is on wrong format, swallow
424                 checksumMatches = false;
425               }
426             } else {
427               checksumMatches = false;
428             }
429           } else {
430             File dest = (File) destination;
431             fos = new FileOutputStream(dest);
432             fos.write(format.format(new Object[]{
433               checksum,
434               src.getName(),
435             }).getBytes());
436             fos.write(System.getProperty("line.separator").getBytes());
437             fos.close();
438             fos = null;
439           }
440         }
441       }
442       if (totalproperty != null) {
443         // Calculate the total checksum
444         // Convert the keys (source files) into a sorted array.
445         Set keys = allDigests.keySet();
446         Object[] keyArray = keys.toArray();
447         // File is Comparable, so sorting is trivial
448         Arrays.sort(keyArray);
449         // Loop over the checksums and generate a total hash.
450         messageDigest.reset();
451         for (int i = 0; i < keyArray.length; i++) {
452           File src = (File) keyArray[i];
453 
454           // Add the digest for the file content
455           byte[] digest = (byte[]) allDigests.get(src);
456           messageDigest.update(digest);
457 
458           // Add the file path
459           String fileName = (String) relativeFilePaths.get(src);
460           messageDigest.update(fileName.getBytes());
461         }
462         //String totalChecksum = createDigestString(messageDigest.digest());
463         //getProject().setNewProperty(totalproperty, totalChecksum);
464       }
465     } catch (Exception e) {
466       throw new RuntimeException(e);
467     } finally {
468       close(fis);
469       close(fos);
470     }
471     return checksumMatches;
472   }
473 
474   /**
475    * Close a Writer without throwing any exception if something went wrong.
476    * Do not attempt to close it if the argument is null.
477    * @param device output writer, can be null.
478    */
479   public static void close(Writer device) {
480       if (device != null) {
481           try {
482               device.close();
483           } catch (IOException ioex) {
484               //ignore
485           }
486       }
487   }
488 
489   /**
490    * Close a stream without throwing any exception if something went wrong.
491    * Do not attempt to close it if the argument is null.
492    *
493    * @param device Reader, can be null.
494    */
495   public static void close(Reader device) {
496       if (device != null) {
497           try {
498               device.close();
499           } catch (IOException ioex) {
500               //ignore
501           }
502       }
503   }
504 
505   public static void close(OutputStream device) {
506       if (device != null) {
507           try {
508               device.close();
509           } catch (IOException ioex) {
510               //ignore
511           }
512       }
513   }
514 
515   /**
516    * Close a stream without throwing any exception if something went wrong.
517    * Do not attempt to close it if the argument is null.
518    *
519    * @param device stream, can be null.
520    */
521   public static void close(InputStream device) {
522       if (device != null) {
523           try {
524               device.close();
525           } catch (IOException ioex) {
526               //ignore
527           }
528       }
529   }
530 
531   private String createDigestString(byte[] fileDigest) {
532     StringBuffer checksumSb = new StringBuffer();
533     for (int i = 0; i < fileDigest.length; i++) {
534       String hexStr = Integer.toHexString(0x00ff & fileDigest[i]);
535       if (hexStr.length() < 2) {
536         checksumSb.append("0");
537       }
538       checksumSb.append(hexStr);
539     }
540     return checksumSb.toString();
541   }
542 
543   /**
544    * Converts an array of characters representing hexadecimal values into an
545    * array of bytes of those same values. The returned array will be half the
546    * length of the passed array, as it takes two characters to represent any
547    * given byte. An exception is thrown if the passed char array has an odd
548    * number of elements.
549    * <p/>
550    * NOTE: This code is copied from jakarta-commons codec.
551    *
552    * @param data an array of characters representing hexadecimal values
553    * @return the converted array of bytes
554    * @throws Exception on error
555    */
556   public static byte[] decodeHex(char[] data) {
557     int l = data.length;
558 
559     if ((l & 0x01) != 0) {
560       throw new RuntimeException("odd number of characters.");
561     }
562 
563     byte[] out = new byte[l >> 1];
564 
565     // two characters form the hex value.
566     for (int i = 0, j = 0; j < l; i++) {
567       int f = Character.digit(data[j++], 16) << 4;
568       f = f | Character.digit(data[j++], 16);
569       out[i] = (byte) (f & 0xFF);
570     }
571 
572     return out;
573   }
574 
575   /**
576    * reads the checksum from a file using the specified format.
577    *
578    * @since 1.7
579    */
580   private String readChecksum(File f) {
581     BufferedReader diskChecksumReader = null;
582     try {
583       diskChecksumReader = new BufferedReader(new FileReader(f));
584       Object[] result = format.parse(diskChecksumReader.readLine());
585       if (result == null || result.length == 0 || result[0] == null) {
586         throw new RuntimeException("failed to find a checksum");
587       }
588       return (String) result[0];
589     } catch (IOException e) {
590       throw new RuntimeException("Couldn't read checksum file " + f, e);
591     } catch (ParseException e) {
592       throw new RuntimeException("Couldn't read checksum file " + f, e);
593     } finally {
594       close(diskChecksumReader);
595     }
596   }
597 
598 }