1 package zipdiff;
2
3 import java.util.*;
4 import java.util.zip.ZipInputStream;
5 import java.util.zip.ZipEntry;
6 import java.util.zip.ZipFile;
7 import java.util.logging.Level;
8 import java.util.logging.Logger;
9 import java.util.regex.*;
10 import java.io.*;
11
12 /***
13 * Checks and compiles differences between two zip files.
14 * It also has the ability to exclude entries from the comparison
15 * based on a regular expression.
16 *
17 * @author Sean C. Sullivan
18 */
19 public class DifferenceCalculator {
20
21 private Logger logger = Logger.getLogger(getClass().getName());
22
23 private ZipFile file1;
24 private ZipFile file2;
25 private boolean ignoreTimestamps = false;
26 private boolean ignoreCVSFiles = false;
27 private boolean compareCRCValues = true;
28 private Pattern filesToIgnorePattern;
29 private boolean bVerbose = false;
30
31 protected void debug(Object msg) {
32 if (isVerboseEnabled()) {
33 System.out.println("[" + DifferenceCalculator.class.getName() + "] " + String.valueOf(msg));
34 }
35 }
36
37 /***
38 * Set the verboseness of the debug output.
39 * @param b true to make verbose
40 */
41 public void setVerbose(boolean b) {
42 bVerbose = b;
43 }
44
45 protected boolean isVerboseEnabled() {
46 return bVerbose;
47 }
48
49 /***
50 * Constructor taking 2 filenames to compare
51 * @throws java.io.IOException
52 */
53 public DifferenceCalculator(String filename1, String filename2) throws java.io.IOException {
54 this(new File(filename1), new File(filename2));
55 }
56
57 /***
58 * Constructor taking 2 Files to compare
59 * @throws java.io.IOException
60 */
61 public DifferenceCalculator(File f1, File f2) throws java.io.IOException {
62 this(new ZipFile(f1), new ZipFile(f2));
63 }
64
65 /***
66 * Constructor taking 2 ZipFiles to compare
67 */
68 public DifferenceCalculator(ZipFile zf1, ZipFile zf2) {
69 file1 = zf1;
70 file2 = zf2;
71 }
72
73 /***
74 *
75 * @param Set A set of regular expressions that when matched against a ZipEntry
76 * then that ZipEntry will be ignored from the comparison.
77 * @see java.util.regex
78 */
79 public void setFilenameRegexToIgnore(Set patterns) {
80 if (patterns == null) {
81 filesToIgnorePattern = null;
82 } else if (patterns.isEmpty()) {
83 filesToIgnorePattern = null;
84 } else {
85 String regex = "";
86
87 Iterator iter = patterns.iterator();
88 while (iter.hasNext()) {
89 String pattern = (String) iter.next();
90 if (regex.length() > 0) {
91 regex += "|";
92 }
93 regex += "(" + pattern + ")";
94 }
95 filesToIgnorePattern = Pattern.compile(regex);
96 logger.log(Level.FINE, "Regular expression is : " + regex);
97 }
98 }
99
100 /***
101 * returns true if fileToIgnorePattern matches the filename given.
102 * @param filepath
103 * @param filename The name of the file to check to see if it should be ignored.
104 * @return true if the file should be ignored.
105 */
106 protected boolean ignoreThisFile(String filepath, String entryName) {
107 if (entryName == null) {
108 return false;
109 }
110 else if (isCVSFile(filepath, entryName) && (ignoreCVSFiles() ) ) {
111 return true;
112 } else if (filesToIgnorePattern == null) {
113 return false;
114 } else {
115 Matcher m = filesToIgnorePattern.matcher(entryName);
116 boolean match = m.matches();
117 if (match) {
118 logger.log(Level.FINEST, "Found a match against : " + entryName + " so excluding");
119 }
120 return match;
121 }
122 }
123
124 protected boolean isCVSFile(String filepath, String entryName)
125 {
126 if (entryName == null)
127 {
128 return false;
129 }
130 else if ( (filepath.indexOf("CVS/") != -1) || (entryName.indexOf("CVS/") != -1))
131 {
132 return true;
133 }
134 else
135 {
136 return false;
137 }
138 }
139 /***
140 * Ensure that the comparison checks against the CRCs of the entries.
141 * @param b true ensures that CRCs will be checked
142 */
143 public void setCompareCRCValues(boolean b) {
144 compareCRCValues = b;
145 }
146
147 /***
148 * @return true if this instance will check the CRCs of each ZipEntry
149 */
150 public boolean getCompareCRCValues() {
151 return compareCRCValues;
152 }
153
154 /***
155 * Opens the ZipFile and builds up a map of all the entries. The key is the name of
156 * the entry and the value is the ZipEntry itself.
157 * @param zf The ZipFile for which to build up the map of ZipEntries
158 * @return The map containing all the ZipEntries. The key being the name of the ZipEntry.
159 * @throws java.io.IOException
160 */
161 protected Map buildZipEntryMap(ZipFile zf) throws java.io.IOException {
162 Map zipEntryMap = new HashMap();
163 try {
164 Enumeration entries = zf.entries();
165 while (entries.hasMoreElements()) {
166 ZipEntry entry = (ZipEntry) entries.nextElement();
167 InputStream is = null;
168 try {
169 is = zf.getInputStream(entry);
170 processZipEntry("", entry, is, zipEntryMap);
171 } finally {
172 if (is != null) {
173 is.close();
174 }
175 }
176 }
177 } finally {
178 zf.close();
179 }
180
181 return zipEntryMap;
182 }
183
184 /***
185 * Will place ZipEntries for a given ZipEntry into the given Map. More ZipEntries will result
186 * if zipEntry is itself a ZipFile. All embedded ZipFiles will be processed with their names
187 * prefixed onto the names of their ZipEntries.
188 * @param prefix The prefix of the ZipEntry that should be added to the key. Typically used
189 * when processing embedded ZipFiles. The name of the embedded ZipFile would be the prefix of
190 * all the embedded ZipEntries.
191 * @param zipEntry The ZipEntry to place into the Map. If it is a ZipFile then all its ZipEntries
192 * will also be placed in the Map.
193 * @param is The InputStream of the corresponding ZipEntry.
194 * @param zipEntryMap The Map in which to place all the ZipEntries into. The key will
195 * be the name of the ZipEntry.
196 * @throws IOException
197 */
198 protected void processZipEntry(String prefix, ZipEntry zipEntry, InputStream is, Map zipEntryMap) throws IOException {
199 if (ignoreThisFile(prefix, zipEntry.getName())) {
200 logger.log(Level.FINE, "ignoring file: " + zipEntry.getName());
201 } else {
202 String name = prefix + zipEntry.getName();
203
204 logger.log(Level.FINEST, "processing ZipEntry: " + name);
205
206 if (zipEntry.isDirectory()) {
207 zipEntryMap.put(name, zipEntry);
208 } else if (isZipFile(name)) {
209 processEmbeddedZipFile(zipEntry.getName() + "/", is, zipEntryMap);
210 zipEntryMap.put(name, zipEntry);
211 } else {
212 zipEntryMap.put(name, zipEntry);
213 }
214 }
215 }
216
217 protected void processEmbeddedZipFile(String prefix, InputStream is, Map m) throws java.io.IOException {
218 ZipInputStream zis = new ZipInputStream(is);
219
220 ZipEntry entry = zis.getNextEntry();
221
222 while (entry != null) {
223 processZipEntry(prefix, entry, zis, m);
224 zis.closeEntry();
225 entry = zis.getNextEntry();
226 }
227
228 }
229
230 /***
231 * Returns true if the filename has a valid zip extension.
232 * i.e. jar, war, ear, zip etc.
233 * @param filename The name of the file to check.
234 * @return true if it has a valid extension.
235 */
236 public static boolean isZipFile(String filename) {
237 boolean result;
238
239 if (filename == null) {
240 result = false;
241 } else {
242 String lowercaseName = filename.toLowerCase();
243 if (lowercaseName.endsWith(".zip")) {
244 result = true;
245 } else if (lowercaseName.endsWith(".ear")) {
246 result = true;
247 } else if (lowercaseName.endsWith(".war")) {
248 result = true;
249 } else if (lowercaseName.endsWith(".rar")) {
250 result = true;
251 } else if (lowercaseName.endsWith(".jar")) {
252 result = true;
253 } else {
254 result = false;
255 }
256 }
257
258 return result;
259 }
260
261 /***
262 * Calculates all the differences between two zip files.
263 * It builds up the 2 maps of ZipEntries for the two files
264 * and then compares them.
265 * @param zf1 The first ZipFile to compare
266 * @param zf2 The second ZipFile to compare
267 * @return All the differences between the two files.
268 * @throws java.io.IOException
269 */
270 protected Differences calculateDifferences(ZipFile zf1, ZipFile zf2) throws java.io.IOException {
271 Map map1 = buildZipEntryMap(zf1);
272 Map map2 = buildZipEntryMap(zf2);
273
274 return calculateDifferences(map1, map2);
275 }
276
277 /***
278 * Given two Maps of ZipEntries it will generate a Differences of all the
279 * differences found between the two maps.
280 * @return All the differences found between the two maps
281 */
282 protected Differences calculateDifferences(Map m1, Map m2) {
283 Differences d = new Differences();
284
285 Set names1 = m1.keySet();
286 Set names2 = m2.keySet();
287
288 Set allNames = new HashSet();
289 allNames.addAll(names1);
290 allNames.addAll(names2);
291
292 Iterator iterAllNames = allNames.iterator();
293 while (iterAllNames.hasNext()) {
294 String name = (String) iterAllNames.next();
295 if (ignoreThisFile("", name)) {
296
297 }
298 else if (names1.contains(name) && (!names2.contains(name))) {
299 d.fileRemoved(name, (ZipEntry) m1.get(name));
300 } else if (names2.contains(name) && (!names1.contains(name))) {
301 d.fileAdded(name, (ZipEntry) m2.get(name));
302 } else if (names1.contains(name) && (names2.contains(name))) {
303 ZipEntry entry1 = (ZipEntry) m1.get(name);
304 ZipEntry entry2 = (ZipEntry) m2.get(name);
305 if (!entriesMatch(entry1, entry2)) {
306 d.fileChanged(name, entry1, entry2);
307 }
308 } else {
309 throw new IllegalStateException("unexpected state");
310 }
311 }
312
313 return d;
314 }
315
316 /***
317 * returns true if the two entries are equivalent in type, name, size, compressed size
318 * and time or CRC.
319 * @param entry1 The first ZipEntry to compare
320 * @param entry2 The second ZipEntry to compare
321 * @return true if the entries are equivalent.
322 */
323 protected boolean entriesMatch(ZipEntry entry1, ZipEntry entry2) {
324 boolean result;
325
326 result =
327 (entry1.isDirectory() == entry2.isDirectory())
328 && (entry1.getSize() == entry2.getSize())
329 && (entry1.getCompressedSize() == entry2.getCompressedSize())
330 && (entry1.getName().equals(entry2.getName()));
331
332 if (!isIgnoringTimestamps()) {
333 result = result && (entry1.getTime() == entry2.getTime());
334 }
335
336 if (getCompareCRCValues()) {
337 result = result && (entry1.getCrc() == entry2.getCrc());
338 }
339 return result;
340 }
341
342 public void setIgnoreTimestamps(boolean b) {
343 ignoreTimestamps = b;
344 }
345
346 public boolean isIgnoringTimestamps() {
347 return ignoreTimestamps;
348 }
349
350 public boolean ignoreCVSFiles()
351 {
352 return ignoreCVSFiles;
353 }
354
355 public void setIgnoreCVSFiles(boolean b)
356 {
357 ignoreCVSFiles = b;
358 }
359
360 /***
361 *
362 * @return all the differences found between the two zip files.
363 * @throws java.io.IOException
364 */
365 public Differences getDifferences() throws java.io.IOException {
366 Differences d = calculateDifferences(file1, file2);
367 d.setFilename1(file1.getName());
368 d.setFilename2(file2.getName());
369
370 return d;
371 }
372 }