Commits

Anonymous committed 0e85306

[FEATURE] Anonymize dicom files coming in using the gradual dicom importer

  • Participants
  • Parent commits 0cb6e43

Comments (0)

Files changed (16)

File plugin-resources/originals/project.xml

 			<type>jar</type>
 		</dependency>
 		<dependency>
+			<groupId>dcm</groupId>
+			<artifactId>Anonymize</artifactId>
+			<version>0.0.1-SNAPSHOT</version>
+			<properties>
+				<war.bundle>true</war.bundle>
+			</properties>
+			<type>jar</type>
+		</dependency>
+		<dependency>
+			<groupId>dcm</groupId>
+			<artifactId>DicomEdit</artifactId>
+			<version>1.5.2</version>
+			<properties>
+				<war.bundle>true</war.bundle>
+			</properties>
+			<type>jar</type>
+		</dependency>
+		<dependency>
+			<groupId>antlr</groupId>
+			<artifactId>antlr-runtime</artifactId>
+			<version>3.2</version>
+			<properties>
+				<war.bundle>true</war.bundle>
+			</properties>
+			<type>jar</type>
+		</dependency>
+		<dependency>
 			<groupId>nrg</groupId>
 			<artifactId>nrg-status</artifactId>
 			<version>2.0.0</version>

File plugin-resources/webapp/xnat/java/org/nrg/xdat/om/base/BaseXnatExperimentdata.java

     	return generator.generateIdentifier();
     }
     
+    /**
+     * newlabel can be null defaults to this.getLabel(), if that is null this.getId()
+     * @param newProject
+     * @param newLabel
+     * @param user
+     * @throws Exception
+     */
     public void moveToProject(XnatProjectdata newProject,String newLabel,XDATUser user) throws Exception{
     	if(!this.getProject().equals(newProject.getId()))
     	{
+    		//Does user have permissions?
     		if(!user.canEdit(this)){
     			throw new InvalidPermissionException(this.getXSIType());
     		}
     		
+    		
     		String existingRootPath=this.getProjectData().getRootArchivePath();
     		
     		if(newLabel==null)newLabel = this.getLabel();
     		if(newLabel==null)newLabel = this.getId();
     		
+    		// newSessionDir = /ARCHIVE/proj_x/arc001
     		final File newSessionDir = new File(new File(newProject.getRootArchivePath(),newProject.getCurrentArc()),newLabel);
     		
+    		// Label defaults to this.getId()
     		String current_label=this.getLabel();
     		if(current_label==null)current_label=this.getId();
     		
+    		
     		for(XnatAbstractresourceI abstRes:this.getResources_resource()){
     			String uri= null;
     			if(abstRes instanceof XnatResource){
     			if(FileUtils.IsAbsolutePath(uri)){
     				int lastIndex=uri.lastIndexOf(File.separator + current_label + File.separator);
     				if(lastIndex>-1)
-    				{
+    				{  
     					lastIndex+=1+current_label.length();
     				}
     				if(lastIndex==-1){

File plugin-resources/webapp/xnat/java/org/nrg/xnat/archive/GradualDicomImporter.java

 import org.dcm4che2.util.TagUtils;
 import org.nrg.action.ClientException;
 import org.nrg.action.ServerException;
+import org.nrg.dcm.Anonymize;
 import org.nrg.dcm.Decompress;
 import org.nrg.dcm.DicomFileNamer;
 import org.nrg.dcm.Extractor;
 import org.nrg.xnat.AbstractDicomObjectIdentifier;
 import org.nrg.xnat.Files;
 import org.nrg.xnat.Labels;
+import org.nrg.xnat.helpers.merge.AnonUtils;
 import org.nrg.xnat.helpers.prearchive.PrearcDatabase;
+import org.nrg.xnat.helpers.prearchive.PrearcDatabase.Either;
 import org.nrg.xnat.helpers.prearchive.PrearcUtils;
 import org.nrg.xnat.helpers.prearchive.SessionData;
 import org.nrg.xnat.helpers.prearchive.SessionException;
             logger.error("unable to parse supplied destination flag", e1);
             throw new ClientException(Status.CLIENT_ERROR_BAD_REQUEST, e1);
         }
+        			
         final String studyInstanceUID = o.getString(Tag.StudyInstanceUID);
         logger.trace("Looking for study {} in project {}", studyInstanceUID,
                 null == project ? null : project.getId());
             root = new File(ArcSpecManager.GetInstance().getGlobalPrearchivePath());
             project_id=null;
         } else {
-            root = new File(project.getPrearchivePath());
+            //root = new File(project.getPrearchivePath());
+        	root = new File (ArcSpecManager.GetInstance().getGlobalPrearchivePath() + "/" + project.getId());
             project_id = project.getId();
         }
         final File tsdir, sessdir;
         sess.setSubject(subject);
 
         sess.setUrl((new File(tsdir,session)).getAbsolutePath());
+        Either<SessionData,SessionData> getOrCreate;
 
-        // query the cache for an existing session that has this Study Instance UID and project name,
-        // if found the SessionData object we just created is over-ridden with the values from the cache
+        // Query the cache for an existing session that has this Study Instance UID and project name.
+        // If found the SessionData object we just created is over-ridden with the values from the cache.
+        // Additionally a record of which operation was performed is contained in the Either<SessionData,SessionData>
+        // object returned. 
+        //
+        // This record is necessary so that, if this row was created by this call, it can be deleted if anonymization
+        // goes wrong. In case of any other error the file is left on the filesystem.
+        // 
+        
         try {
-            sess = PrearcDatabase.getOrCreateSession(sess.getProject(), sess.getTag(), sess, tsdir, shouldAutoArchive(o));
+            getOrCreate = PrearcDatabase.eitherGetOrCreateSession(sess.getProject(), sess.getTag(), sess, tsdir, shouldAutoArchive(o));
+            if (getOrCreate.isLeft()) {
+            	sess = getOrCreate.getLeft();
+            }
+            else { 
+            	sess = getOrCreate.getRight();
+            }
             PrearcDatabase.setLastModifiedTime(sess.getName(), sess.getTimestamp(), sess.getProject());        
         } catch (SQLException e) {
             throw new ServerException(Status.SERVER_ERROR_INTERNAL, e);
 
             final File f = getSafeFile(sessdir, scan, name, o, Boolean.valueOf((String)params.get(RENAME_PARAM)));
             f.getParentFile().mkdirs();
+            
             try {
                 write(fmi, o, bis, f, source);
+                    
             } catch (IOException e) {
                 throw new ServerException(Status.SERVER_ERROR_INSUFFICIENT_STORAGE, e);
             }
+            try {
+                File defaultAnonScript = AnonUtils.getDefaultScript();
+            	Anonymize.anonymize(f, sess.getProject(), sess.getSubject(), sess.getFolderName(), defaultAnonScript, null);
+            } catch (Throwable e) {
+            	logger.debug("Dicom anonymization failed: " + f, e);
+        		try {
+        			// if we created a row in the database table for this session
+        			// delete it.
+        			if (getOrCreate.isRight()) {
+        					PrearcDatabase.deleteSession(sess.getFolderName(), sess.getTimestamp(), sess.getProject());
+        			}
+        			else {
+        				f.delete();
+        			}
+        		}
+        		catch (Throwable t) {
+        			logger.debug("Unable to delete relevant file :" + f, e);
+        			throw new ServerException(Status.SERVER_ERROR_INTERNAL, t);
+            	}
+        		throw new ServerException(Status.SERVER_ERROR_INTERNAL, e);
+            }
+
         } finally {
             if (null != dis) try {
                 dis.close();
                 new Object[]{project, studyInstanceUID, o.getString(Tag.SOPInstanceUID), sess.getUrl(), source});
         return Collections.singletonList(sess.getExternalUrl());
     }
-
-    public void setCacheManager(final CacheManager cacheManager) {
+    
+	public void setCacheManager(final CacheManager cacheManager) {
         final String cacheName = user.getLogin() + "-projects";
         synchronized (cacheManager) {
             if (!cacheManager.cacheExists(cacheName)) {
         }
         return null;
     }
-
+    
     private static final class RuleBasedIdentifier
     extends AbstractDicomObjectIdentifier<XnatProjectdata> {
         private static final Extractor[] exts;

File plugin-resources/webapp/xnat/java/org/nrg/xnat/archive/Rename.java

 import org.nrg.xft.exception.FieldNotFoundException;
 import org.nrg.xft.exception.XFTInitException;
 import org.nrg.xnat.exceptions.InvalidArchiveStructure;
+import org.nrg.xnat.helpers.merge.AnonymizeSession;
 import org.nrg.xnat.turbine.utils.ArchivableItem;
 import org.nrg.xnat.utils.WorkflowUtils;
 
 		}
 	}
 	
+	
+	
 	/**
 	 * Rename the label for the corresponding session and modify the file URIs for the adjusted path.
 	 * 
 				generateURISQL(i, expected, newArchive, cache, user);
 
 				this.updateStep(workflow, setStep(STEP.COPY_DIR));
+
+				if(moveFiles)org.nrg.xft.utils.FileUtils.CopyDir(oldSessionDir, newSessionDir,false);	
 				
-				if(moveFiles)org.nrg.xft.utils.FileUtils.CopyDir(oldSessionDir, newSessionDir,false);	
-
+				String projectLabel = proj.getName();
+				XnatImagesessiondata src = (XnatImagesessiondata)XnatExperimentdata.GetExptByProjectIdentifier(proj.getName(), newLabel, null, false);
+				new AnonymizeSession(projectLabel, src).anonymize();
+				
 				this.updateStep(workflow, setStep(STEP.EXECUTE_SQL));
 				//Execute SQL
 				executeSQL(cache,user,XFT.buildLogFileName(i));

File plugin-resources/webapp/xnat/java/org/nrg/xnat/helpers/merge/AnonymizeSession.java

+package org.nrg.xnat.helpers.merge;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.nrg.dcm.Anonymize;
+import org.nrg.dcm.AnonymizerI;
+import org.nrg.dcm.edit.AttributeException;
+import org.nrg.dcm.edit.ScriptEvaluationException;
+import org.nrg.xdat.model.XnatAbstractresourceI;
+import org.nrg.xdat.model.XnatImagescandataI;
+import org.nrg.xdat.om.XnatAbstractresource;
+import org.nrg.xdat.om.XnatExperimentdata;
+import org.nrg.xdat.om.XnatImagescandata;
+import org.nrg.xdat.om.XnatImagesessiondata;
+import org.nrg.xdat.om.XnatProjectdata;
+import org.nrg.xdat.om.XnatResource;
+import org.nrg.xnat.helpers.merge.AnonUtils;
+
+public final class AnonymizeSession implements AnonymizerI {
+	
+	final XnatImagesessiondata src; // session to anonymize
+	final String project; // Use the anonymization script from this project
+	
+	public AnonymizeSession(String project, XnatImagesessiondata src) {
+		this.src = src;
+		this.project = project;
+	}
+
+	@Override
+	public void anonymize() throws AttributeException,
+			ScriptEvaluationException, FileNotFoundException, IOException {
+		String session = src.getLabel();
+		String subject = src.getSubjectData().getLabel();
+		String project = src.getProject();
+		XnatProjectdata pd = XnatProjectdata.getProjectByIDorAlias(project, null, false);
+		String destRootPath = pd.getRootArchivePath();
+		
+		String rootpath = src.getArchivePath();
+		// anonymize everything in srcRootPath
+		List<XnatImagescandataI> l = src.getScans_scan();
+		Iterator<XnatImagescandataI> i = l.iterator();
+		while(i.hasNext()) {
+			XnatImagescandata x = (XnatImagescandata)i.next();
+			List<XnatAbstractresourceI> rs = x.getFile();
+			Iterator<XnatAbstractresourceI> rs_i = rs.iterator();
+			while (rs_i.hasNext()) {
+				XnatAbstractresource abs = (XnatAbstractresource) rs_i.next();
+				if (abs instanceof XnatResource) {
+					if (abs.getFormat().equals("DICOM")){
+						ArrayList<File> fs = abs.getCorrespondingFiles(rootpath);
+						Iterator<File> f_i = fs.iterator();
+						while (f_i.hasNext()) {
+							File f = f_i.next();
+							File anonScript = AnonUtils.getAnonScript(destRootPath);
+			                File defaultAnonScript = AnonUtils.getDefaultScript();
+			                Anonymize.anonymize(f,project,subject,session,defaultAnonScript,anonScript);
+						}
+					}
+				}
+			}
+		}
+	}
+
+}

File plugin-resources/webapp/xnat/java/org/nrg/xnat/helpers/merge/CopyOp.java

+package org.nrg.xnat.helpers.merge;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+import org.nrg.xft.utils.FileUtils;
+
+public final class CopyOp<A> extends Transaction{
+	final Callable<A> op;
+	final File dir;
+	final File rootBackup;
+	final String backupDirName;
+	File backup = null;
+	final LoggerI logger;
+	
+	public interface LoggerI {
+		void info(String l);
+		void failed(String l);
+	}
+
+	public CopyOp(Callable<A>op, File dir, File rootBackup, String backupDirName) {
+		this.op = op;
+		this.dir = dir;
+		this.rootBackup = rootBackup;
+		this.backupDirName = backupDirName;
+		this.logger = new LoggerI() {
+			public void info (String l) {}
+			public void failed (String l) {}
+		};
+	}
+	public CopyOp(Callable<A>op, File dir, File rootBackup, String backupDirName, LoggerI logger) {
+		this.op = op;
+		this.dir = dir;
+		this.rootBackup = rootBackup;
+		this.backupDirName = backupDirName;
+		this.logger = logger;
+	}
+
+	private File copy() throws TransactionException {
+		File backup = new File(rootBackup,this.backupDirName);
+		backup.mkdirs();
+		
+		logger.info("Backing up source directory");
+		try {
+			FileUtils.CopyDir(dir, backup, false);
+		} catch (Exception e) {
+			logger.failed("Failed to backup source directory");
+			throw new TransactionException (e.getMessage(),e);
+		}
+		return backup;
+	}
+	
+	
+	public void run() throws TransactionException {
+		this.backup = this.copy();
+		try {
+			this.op.call();
+		}
+		catch (Throwable e){
+			throw new TransactionException(e.getMessage(),e);
+		}
+	}
+	
+	private void rollbackHelper(File backupDIR, File destDIR, boolean overwrite)  throws RollbackException{
+		File backup = new File(rootBackup,"modified_dest");
+		backup.mkdirs();
+
+		logger.info("Restoring previous version of destination directory.");
+		try {
+			FileUtils.MoveDir(destDIR, backup, true);
+		} catch (IOException e) {
+			logger.info(e.getMessage());
+		}
+		
+		try {
+			FileUtils.MoveDir(backupDIR, destDIR, overwrite);
+		} catch (Exception e) {
+			logger.failed("Failed to restore previous version of destination directory.");
+			throw new RollbackException(e.getMessage(),e);
+		}
+	}
+
+	public void rollback() throws RollbackException {
+		try {
+			rollbackHelper(backup,dir,true);	
+		}
+		catch (RollbackException e) {
+			logger.failed("Failed to restore previous version of destination directory.");
+			throw e;
+		}
+	}
+}

File plugin-resources/webapp/xnat/java/org/nrg/xnat/helpers/merge/MergePrearcToArchiveSession.java

 package org.nrg.xnat.helpers.merge;
 
 import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
+import java.util.concurrent.Callable;
 
 import org.nrg.action.ClientException;
 import org.nrg.action.ServerException;
+import org.nrg.dcm.Anonymize;
+import org.nrg.dcm.edit.AttributeException;
+import org.nrg.dcm.edit.ScriptEvaluationException;
 import org.nrg.xdat.model.XnatAbstractresourceI;
 import org.nrg.xdat.model.XnatImagescandataI;
 import org.nrg.xdat.model.XnatResourceI;
 import org.nrg.xdat.model.XnatResourcecatalogI;
 import org.nrg.xdat.model.XnatResourceseriesI;
 import org.nrg.xdat.om.XnatAbstractresource;
+import org.nrg.xdat.om.XnatImagescandata;
 import org.nrg.xdat.om.XnatImagesessiondata;
 import org.nrg.xdat.om.XnatResource;
+import org.nrg.xnat.helpers.merge.AnonUtils;
 import org.nrg.xnat.turbine.utils.XNATUtils;
 import org.restlet.data.Status;
 
 public  class  MergePrearcToArchiveSession extends  MergeSessionsA<XnatImagesessiondata> {
 	public MergePrearcToArchiveSession(Object control,final File srcDIR, final XnatImagesessiondata src, final String srcRootPath, final File destDIR, final XnatImagesessiondata existing, final String destRootPath, boolean overwrite, boolean allowDataDeletion,SaveHandlerI<XnatImagesessiondata> saver) {
 		super(control, srcDIR, src, srcRootPath, destDIR, existing, destRootPath, overwrite, allowDataDeletion,saver);
+		super.setAnonymizer(new AnonymizeSession(existing.getProject(), src));
 	}
 
 

File plugin-resources/webapp/xnat/java/org/nrg/xnat/helpers/merge/MergeSessionsA.java

 
 import java.io.File;
 import java.io.FileFilter;
+import java.io.FileNotFoundException;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.Callable;
 
 import org.apache.log4j.Logger;
 import org.nrg.action.ClientException;
 import org.nrg.action.ServerException;
+import org.nrg.dcm.edit.AttributeException;
+import org.nrg.dcm.edit.ScriptEvaluationException;
 import org.nrg.status.StatusProducer;
 import org.nrg.xdat.bean.CatCatalogBean;
 import org.nrg.xdat.model.XnatImagesessiondataI;
 import org.nrg.xnat.helpers.merge.MergeCatCatalog.DCMEntryConflict;
 import org.nrg.xnat.utils.CatalogUtils;
 import org.restlet.data.Status;
+import org.nrg.xnat.helpers.merge.CopyOp;
 
 public abstract class MergeSessionsA<A extends XnatImagesessiondataI> extends StatusProducer implements Callable<A> {
 	public static final String CAT_ENTRY_MATCH = "Session already exists with the same resources.  Retry with overwrite=delete to force an overwrite of the pre-existing data.";
 	protected final String destRootPath,srcRootPath;	
 	protected final boolean overwrite,allowDataDeletion;
 	protected final SaveHandlerI<A> saver;
+	protected ArrayList<Callable<A>> befores = new ArrayList<Callable<A>>();
+	protected AnonymizeSession anonymizer = null;
 	protected final Object control;
-
+	
 	static org.apache.log4j.Logger logger = Logger.getLogger(MergeSessionsA.class);
 	
 	public MergeSessionsA(Object control,final File srcDIR, final A src, final String srcRootPath, final File destDIR, final A existing, final String destRootPath, boolean overwrite, boolean allowDataDeletion,SaveHandlerI<A> saver) {
 		this.saver=saver;
 	}
 	
+	protected void setAnonymizer(AnonymizeSession a) {
+		this.anonymizer = a;
+	}
+	
 	public interface SaveHandlerI<A>{
 		public void save(A session) throws Exception;
 	}
+	public interface AnonymizerI {
+		public void anonymize() throws AttributeException, ScriptEvaluationException, FileNotFoundException, IOException;
+	}
 	
 	public void checkForConflict() throws ClientException,ServerException{
 		if(destDIR.exists() || dest!=null){
 		if(dest!=null){
 			backupXML(dest,rootBackup);
 		}
+		
+		File srcBackupDir = null;
+		if (anonymizer != null) {
+			// copy source directory
+			srcBackupDir = backupSourceDIR(srcDIR, rootBackup);
+		}
 
 		
 		final UpdatedSession<A> update=mergeSessions(src,srcRootPath,dest,destRootPath);
 		//DONE
 		
 		finalize(merged);
+		
+		final A completedMerge = merged;
 
 		deleteCatalogFiles(update.getToDelete(),rootBackup);
 		
 		mergeDirectories(srcDIR,destDIR,allowDataDeletion);
 		
+		CopyOp.LoggerI logger = new CopyOp.LoggerI(){
+			public void info(String l) {processing(l);}
+			public void failed(String l) {failed(l);}
+		};
+		
+		CopyOp<java.lang.Void> anonymizerOp = null; 
+		if (anonymizer != null) {
+			anonymizerOp = new CopyOp<java.lang.Void>(new Callable<java.lang.Void>(){
+				public java.lang.Void call() throws Exception {
+					anonymizer.anonymize();
+					return null;
+				}
+			}, srcDIR, rootBackup, "src_backup", logger);
+		}		
+		
+		CopyOp<java.lang.Void> saverOp = new CopyOp<java.lang.Void>(new Callable<java.lang.Void>(){
+			public java.lang.Void call() throws Exception {
+					saver.save(completedMerge);
+					return null;
+			}
+		}, destDIR, rootBackup, "dest_backup", logger);
+		
+		Transaction t = anonymizer != null ? anonymizerOp.bind(saverOp) : saverOp;
+		
 		try {
-			this.processing("Updating stored meta-data.");
-			saver.save(merged);
-		} catch (Throwable e) {
-			if(backupDIR!=null){
-				rollback(backupDIR,destDIR,rootBackup);
-			}else{
-				rollback(destDIR,srcDIR,rootBackup);
-			}
-			failed("Error updating existing meta-data");
-			throw new ServerException(Status.SERVER_ERROR_INTERNAL,e.getMessage(), new Exception());
+			Run.runTransaction(t);
+		}
+		catch (TransactionException e) {
+			throw new ServerException(Status.SERVER_ERROR_INTERNAL,e.getMessage(), e);
+		}
+		catch (RollbackException e) {
+			throw new ServerException(Status.SERVER_ERROR_INTERNAL,e.getMessage(), e);
 		}
 		
 		return merged;
 		
 		return backup;
 	}
-
+	
+	private File backupSourceDIR (File sourceDIR2, File rootBackup)throws ServerException {
+		File backup = new File(rootBackup,"src_backup");
+		backup.mkdirs();
+		
+		this.processing("Backing up source directory");
+		try {
+			FileUtils.CopyDir(sourceDIR2, backup, false);
+		} catch (Exception e) {
+			this.failed("Failed to backup source directory");
+			throw new ServerException(e.getMessage(),e);
+		}
+		
+		return backup;
+	}
+	
 	private File createPrimaryBackupDirectory(String cacheBKDirName,
 			String project,String folderName) {
 		File f= org.nrg.xnat.utils.FileUtils.buildCachepath(project, cacheBKDirName, folderName);

File plugin-resources/webapp/xnat/java/org/nrg/xnat/helpers/merge/RollbackException.java

+package org.nrg.xnat.helpers.merge;
+
+public class RollbackException extends Exception {
+    /**
+     * 
+     */
+    public RollbackException() {
+    }
+
+    /**
+     * @param message
+     */
+    public RollbackException(String message) {
+        super(message);
+    }
+
+    /**
+     * @param cause
+     */
+    public RollbackException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * @param message
+     * @param cause
+     */
+    public RollbackException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

File plugin-resources/webapp/xnat/java/org/nrg/xnat/helpers/merge/Run.java

+package org.nrg.xnat.helpers.merge;
+
+public final class Run {
+	public static void runTransaction (Transaction t) throws TransactionException, RollbackException {
+		Transaction n = t;
+		try {
+			n.run();
+			while ((n = n.getNext()) != null){
+				n.run(); 
+			}
+		}
+		catch (TransactionException e) {
+			n.rollback();
+			Transaction p = n;
+			while ((p = p.getPrev()) != null){
+				p.rollback();
+			}
+			throw e;
+		}
+	}
+}

File plugin-resources/webapp/xnat/java/org/nrg/xnat/helpers/merge/Transaction.java

+package org.nrg.xnat.helpers.merge;
+
+public abstract class Transaction {
+	public Transaction prev = null;
+	public Transaction next = null;
+	
+	public abstract void run() throws TransactionException;
+	public abstract void rollback() throws RollbackException;
+	
+	public Transaction bind(Transaction t) {
+		this.setNext(t);
+		t.setPrev(this);
+		return t;
+	}
+
+	public Transaction getNext() {
+		return this.next;
+	}
+
+	public Transaction getPrev() {
+		return this.prev;
+	}
+	
+	public void setPrev(Transaction p) {
+		this.prev = p;
+	}
+	
+	public void setNext(Transaction n) {
+		this.next = n;
+	}
+}

File plugin-resources/webapp/xnat/java/org/nrg/xnat/helpers/merge/TransactionException.java

+package org.nrg.xnat.helpers.merge;
+@SuppressWarnings("serial")
+public class TransactionException extends Throwable {
+    /**
+     * 
+     */
+    public TransactionException() {
+    }
+
+    /**
+     * @param message
+     */
+    public TransactionException(String message) {
+        super(message);
+    }
+
+    /**
+     * @param cause
+     */
+    public TransactionException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * @param message
+     * @param cause
+     */
+    public TransactionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

File plugin-resources/webapp/xnat/java/org/nrg/xnat/helpers/prearchive/PrearcDatabase.java

      * @param <Left> Return type if the left branch of tree is taken.
      * @param <Right> Return type if the right branch of the tree is taken.
      */
-    static abstract class Either<Left,Right> {
+    public static abstract class Either<Left,Right> {
+    	enum Eithers {LEFT,RIGHT};
         // typically the result of an error
         Left l;
         // typically the result of a successful operation
         Right r;
         // true if Right is not null, false if Left is not null. 
-        boolean set;
+        Eithers set;
         Either<Left,Right> setLeft(Left l) {
-            this.set = false;
+            this.set = Eithers.LEFT;
             this.l = l;
             return this;
         }
         Either<Left,Right> setRight(Right r) {
-            this.set = true;
+            this.set = Eithers.RIGHT;
             this.r = r;
             return this;
         }
-        Left getLeft(){
-            this.set = false;
+        public Left getLeft(){
             return this.l;
         }
-        Right getRight() {
+        public Right getRight() {
             return this.r;
         }
-        boolean isLeft() {
-            return this.set == false;
+        public boolean isLeft() {
+            return this.set == Eithers.LEFT;
         }
-        boolean isRight() {
-            return this.set == true;
+        public boolean isRight() {
+            return this.set == Eithers.RIGHT;
         }
     }
 
      *  
      * @author aditya
      *
-     * @param <X> Return type if the predicate holds
-     * @param <Y> Return type if the predicate fails
+     * @param <X> Return type if the predicate fails
+     * @param <Y> Return type if the predicate holds
      */
     static abstract class PredicatedOp<X,Y> {
         // the predicate
             }
         }.run();
     }
-
+    
     /**
-     * Either retrieve and existing session or create a new one.
+     * Either retrieve and existing session or create a new one and return it.
+     * 
+     * This function is useful if the caller does not care which operation was performed.
+     * 
      * @param project
      * @param suid
      * @param s
      * @throws SessionException
      * @throws Exception
      */
-    public static synchronized SessionData getOrCreateSession (final String project, final String suid, final SessionData s, final File tsFile, final Boolean autoArchive) throws SQLException, SessionException, Exception {
+    public static SessionData getOrCreateSession(final String project,
+    				     					     final String suid,
+    				     					     final SessionData s,
+    				     					     final File tsFile,
+    				     					     final Boolean autoArchive)
+    throws SQLException, SessionException, Exception {
+    	Either<SessionData, SessionData> result = PrearcDatabase.eitherGetOrCreateSession(project, suid, s, tsFile, autoArchive);
+        if (result.isLeft()) {
+            return result.getLeft();
+        }
+        else{
+            return result.getRight();
+        }
+    }
+
+    /**
+     * Either retrieve and existing session or create a new one. If a session is created an Either
+     * object with the "Right" branch set is returned. If we just retrieve one that is already in the
+     * prearchive table an Either object with the "Left" branch set is returned.
+     * 
+     * This is useful in case the caller needs to know which operation was performed.
+     * @param project
+     * @param suid
+     * @param s
+     * @param tsFile
+     * @param autoArchive
+     * @return
+     * @throws SQLException
+     * @throws SessionException
+     * @throws Exception
+     */
+    public static synchronized Either<SessionData,SessionData> eitherGetOrCreateSession (final String project, final String suid, final SessionData s, final File tsFile, final Boolean autoArchive) throws SQLException, SessionException, Exception {
         Either<SessionData,SessionData> result = new PredicatedOp<SessionData,SessionData>() {
             ResultSet rs;
             /**
                 }.run();
             }
         }.run();
+        
+        return result;
 
-        if (result.isLeft()) {
-            return result.getLeft();
-        }
-        else{
-            return result.getRight();
-        }
     }
 
     /**

File plugin-resources/webapp/xnat/javatest/org/nrg/xnat/helpers/merge/CopyOpTest.java

+package org.nrg.xnat.helpers.merge;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CopyOpTest {
+	static File tmpDir = new File(System.getProperty("java.io.tmpdir"));
+	static File dirA;
+	static File backupDir;
+	static File dirB;
+
+	@Before
+	public void setUp() throws Exception {
+		dirA = new File(tmpDir,"a");
+		if (dirA.exists()) {
+			FileUtils.deleteDirectory(dirA);
+		}
+		
+		backupDir = new File(tmpDir,"backup");
+		if (backupDir.exists()) {
+			FileUtils.deleteDirectory(backupDir);
+		}
+		dirA.mkdirs();
+		backupDir.mkdirs();
+		addToFile("Hello Java");
+	}	
+
+	@After
+	public void tearDown() throws Exception {
+		if (dirA != null) {
+			FileUtils.deleteDirectory(dirA);
+		}
+		if (backupDir != null) {
+			FileUtils.deleteDirectory(backupDir);
+		}
+	}
+	
+	public void addToFile(String l) throws IOException {
+		FileWriter fstream = new FileWriter(new File(dirA, "out.txt"),false);
+		BufferedWriter out = new BufferedWriter(fstream);
+		out.write(l);
+		out.close();
+	}
+	
+	
+	
+	public String readFile() throws IOException{
+		StringBuffer fileData = new StringBuffer(1000);
+		BufferedReader reader = new BufferedReader(
+				new FileReader(new File(dirA,"out.txt")));
+		char[] buf = new char[1024];
+		int numRead=0;
+		while((numRead=reader.read(buf)) != -1){
+			String readData = String.valueOf(buf, 0, numRead);
+			fileData.append(readData);
+			buf = new char[1024];
+		}
+		reader.close();
+		return fileData.toString();
+	}
+	
+	@Test
+	public final void successfulTest() {
+		Callable<Void> c = new Callable<Void>(){
+			@Override
+			public Void call() throws Exception {
+				addToFile("Goodbye Java");
+				return null;
+			}
+			
+		};
+		CopyOp<Void> op = new CopyOp<Void>(c,dirA,backupDir,"src_dir");
+		
+		try {
+			Run.runTransaction(op);
+		} catch (RollbackException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (TransactionException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+		String fileContents = null;
+		try {
+			fileContents = readFile();
+		} catch (IOException e) {
+			fail("");
+		}
+		assertEquals(fileContents,"Goodbye Java");
+	}
+	
+	@Test
+	public final void unSuccessfulTest() {
+		Callable<Void> c = new Callable<Void>(){
+			@Override
+			public Void call() throws Exception {
+				addToFile("Goodbye Java");
+				throw new Exception();
+			}
+			
+		};
+		CopyOp<Void> op = new CopyOp<Void>(c,dirA,backupDir,"src_dir");
+		
+		try {
+			Run.runTransaction(op);
+		} catch (RollbackException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		} catch (TransactionException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+		String fileContents = null;
+		try {
+			fileContents = readFile();
+		} catch (IOException e) {
+			fail("");
+		}
+		assertEquals(fileContents,"Hello Java");
+	}
+}

File plugin-resources/webapp/xnat/javatest/org/nrg/xnat/helpers/merge/RunTest.java

+package org.nrg.xnat.helpers.merge;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+
+import org.apache.commons.lang.StringUtils;
+import org.junit.Test;
+
+
+
+public class RunTest {
+	@Test
+	public final void testRollbackA() {
+		final ArrayList<String> s = new ArrayList<String>();
+		Transaction a = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run a");
+				throw new TransactionException();
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback a");
+			}
+		};
+		Transaction b = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run b");
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback b");
+			}
+		};
+		
+		Transaction c = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run c");
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback c");
+			}
+		};
+		a.bind(b).bind(c);
+		try {
+			Run.runTransaction(a);
+		} catch (RollbackException e) {
+			fail("");
+		} catch (TransactionException e) {
+			String x = StringUtils.join(s, ',');
+			assertEquals(x, "Run a,Rollback a");
+		}
+	}
+	
+	@Test
+	public final void testRollbackB() {
+		final ArrayList<String> s = new ArrayList<String>();
+		Transaction a = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run a");
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback a");
+			}
+		};
+		Transaction b = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run b");
+				throw new TransactionException();
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback b");
+			}
+		};
+		
+		Transaction c = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run c");
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback c");
+			}
+		};
+		a.bind(b).bind(c);
+		try {
+			Run.runTransaction(a);
+		} catch (RollbackException e) {
+			fail("");
+		} catch (TransactionException e) {
+			String x = StringUtils.join(s, ',');
+			assertEquals(x, "Run a,Run b,Rollback b,Rollback a");
+		}
+	}
+	
+	@Test
+	public final void testRollbackC() {
+		final ArrayList<String> s = new ArrayList<String>();
+		Transaction a = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run a");
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback a");
+			}
+		};
+		Transaction b = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run b");
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback b");
+			}
+		};
+		
+		Transaction c = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run c");
+				throw new TransactionException();
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback c");
+			}
+		};
+		a.bind(b).bind(c);
+		try {
+			Run.runTransaction(a);
+		} catch (RollbackException e) {
+			fail("");
+		} catch (TransactionException e) {
+			String x = StringUtils.join(s, ',');
+			assertEquals(x, "Run a,Run b,Run c,Rollback c,Rollback b,Rollback a");
+		}
+	}
+	@Test
+	public final void testRunSuccessful() {
+		final ArrayList<String> s = new ArrayList<String>();
+		Transaction a = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run a");
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback a");
+			}
+		};
+		Transaction b = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run b");
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback b");
+			}
+		};
+		
+		Transaction c = new Transaction() {
+			@Override
+			public void run() throws TransactionException {
+				s.add("Run c");
+			}
+			@Override
+			public void rollback() throws RollbackException {
+				s.add("Rollback c");
+			}
+		};
+		a.bind(b).bind(c);
+		try {
+			Run.runTransaction(a);
+		} catch (RollbackException e) {
+			fail("");
+		} catch (TransactionException e) {
+			fail("");
+		}
+		String x = StringUtils.join(s, ',');
+		assertEquals(x, "Run a,Run b,Run c");
+	}
+}

File sample.classpath

   <classpathentry kind="lib" path="plugin-resources/maven-1.0.2/temp/isorelax/jars/isorelax-20030108.jar"/>
   <classpathentry kind="lib" path="plugin-resources/maven-1.0.2/temp/jsch/jars/jsch-0.1.5.jar"/>
   <classpathentry kind="lib" path="plugin-resources/repository/ant/jars/ant-1.6.1.jar"/>
+  <classpathentry kind="lib" path="plugin-resources/repository/antlr/jars/antlr-runtime-3.2.jar"/>
   <classpathentry kind="lib" path="plugin-resources/repository/avalon-framework/jars/avalon-framework-4.1.4.jar"/>
   <classpathentry kind="lib" path="plugin-resources/repository/axis-ant/jars/axis-ant-1_3.jar"/>
   <classpathentry kind="lib" path="plugin-resources/repository/axis-schema/jars/axis-schema-1_3.jar"/>
 	<classpathentry kind="lib" path="plugin-resources/repository/dcm/jars/dicom-xnat-sop-1.5.1.jar"/>
 	<classpathentry kind="lib" path="plugin-resources/repository/dcm/jars/dicom-xnat-util-1.5.1.jar"/>
 	<classpathentry kind="lib" path="plugin-resources/repository/dcm/jars/ExtAttr-2.0.0.jar"/>
+	<classpathentry kind="lib" path="plugin-resources/repository/dcm/jars/DicomEdit-1.5.2.jar"/>
+	<classpathentry kind="lib" path="plugin-resources/repository/dcm/jars/Anonymize-0.0.1-SNAPSHOT.jar"/>
 	<classpathentry kind="lib" path="plugin-resources/repository/nrg/jars/nrg-status-2.0.0.jar"/>
 	<classpathentry kind="lib" path="plugin-resources/repository/dcm/jars/PrearcImporter-1.5.1.jar"/>
 	<classpathentry kind="lib" path="plugin-resources/repository/dcm/jars/SessionBuilders-1.5.0.jar"/>