package sandbox.changeset; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class ClassFilesConcurrent { private final ConcurrentHashMap hm = new ConcurrentHashMap(); private final ConcurrentHashMap bbuffers = new ConcurrentHashMap(); private final ExecutorService es = Executors.newCachedThreadPool(); private final ConcurrentLinkedQueue errors = new ConcurrentLinkedQueue(); private final File baseOld; private final File baseNew; public ClassFilesConcurrent(File pathOld, File pathNew){ baseOld = pathOld; baseNew = pathNew; es.execute(new DirExaminer(true)); es.execute(new DirExaminer(false)); } public ChangeSet getResults(){ while(!es.isTerminated()){ try { es.awaitTermination(1L, TimeUnit.SECONDS); } catch (InterruptedException e) {} } return new ChangeSet(hm); } private class DirExaminer implements Runnable{ private final File base; private final Status status; public DirExaminer(boolean old){ base = old? baseOld : baseNew; status = old? Status.OLD : Status.NEW; } @Override public void run() { examineDir(""); if(hm.putIfAbsent("", Status.SAME) != null){ hm.remove(""); es.shutdown(); } } private void examineDir(final String rel){ String[] ls = new File(base,rel).list(); if(ls==null) //dir is not a directory return; for (String cur : ls) { if(cur.endsWith(".class")){ cur = (rel+cur).intern(); if(hm.putIfAbsent(cur, status) != null) es.execute(new FileComparator(cur)); } else { examineDir(rel+cur+File.separator);} } } } private class FileComparator implements Runnable{ private static final int BYTEBUFFER_CAPACITY = 65536; private final String cn; public FileComparator(final String className){ cn = className; } @Override public void run() { FileChannel fcOld = null, fcNew = null; try { fcOld = new FileInputStream(new File(baseOld,cn)).getChannel(); fcNew = new FileInputStream(new File(baseNew,cn)).getChannel(); if(filesDiffer(fcOld, fcNew)) hm.replace(cn, Status.CHANGED); else hm.replace(cn, Status.SAME); } catch (FileNotFoundException e) { errors.add(e.toString()); } catch (IOException e) { errors.add(e.toString()); } finally{ closeFC(baseOld, fcOld); closeFC(baseNew, fcNew); } } private boolean filesDiffer(FileChannel fc0, FileChannel fc1) throws IOException{ //If the file size is different, the class must have changed! if(fc0.size()!=fc1.size()){ return true; } /*Get or Instantiate buffers. The Thread-id is a trick: * A thread will reuse two bytebuffer instances for all tasks assigned to it. * And no other thread will ever (concurrently) use them. */ ByteBuffer b0, b1; Long id = Thread.currentThread().getId(); if ((b1 = bbuffers.get(id))==null){ b1 = ByteBuffer.allocateDirect(BYTEBUFFER_CAPACITY); b0 = ByteBuffer.allocateDirect(BYTEBUFFER_CAPACITY); bbuffers.put(id, b1); bbuffers.put(-id, b0); } else{ b0 = bbuffers.get(-id); } /* ByteBuffer usage: * read(b0) == read(b1) => ensure equal number of bytes are read (=put) into buffer * b0.flip(); b1.flip() -> ready to read (=compare) out of buffer. * ALSO: 'hides' stale (unused) bytes. * So never compare before flipping! * b0.compareTo(b1) -> 0 indicates equal * * In sum: read, flip, compare, clear. * */ boolean more0 = true, more1 = true; while(more0){ b0.clear(); b1.clear(); assert b0.remaining()==BYTEBUFFER_CAPACITY && b1.remaining()==BYTEBUFFER_CAPACITY && more0==more1; while( ( more0 = (fc0.read(b0)!=-1) ) && b0.hasRemaining()){} while( ( more1 = (fc1.read(b1)!=-1) ) && b1.hasRemaining()){} assert (b0.remaining()==b1.remaining() && more0==more1); b0.flip(); b1.flip(); if(b0.compareTo(b1)!=0){ //Buffer contents differ, ergo files differ. return true; } } //The files were completely read without encountering changes return false; } //Close a filechannel, handle errors. private void closeFC(File base, FileChannel fc){ if (fc==null) return; try { fc.close(); } catch (IOException e) { errors.add("DANGER! Possible corrupt FileSytem!!! Could not close opened file: " + base.toString() + cn); } } } }