Difference between revisions of "Developing a DicomDifferenceLogger LogAdapter"

From MircWiki
Jump to: navigation, search
(An Example AnonymizerExtension)
(An Example LogAdapter)
 
(7 intermediate revisions by the same user not shown)
Line 48: Line 48:
 
//of element. By convention, the name of the child element is the name of
 
//of element. By convention, the name of the child element is the name of
 
//the LogAdapter class (in this case, ExampleLogAdapter).  
 
//the LogAdapter class (in this case, ExampleLogAdapter).  
//
 
//In more complex situations, you may have to construct the cohortName by
 
//interrogating either or both of the current and cached objects.
 
 
String className = getClass().getName();
 
String className = getClass().getName();
 
className = className.substring(className.lastIndexOf(".")+1);
 
className = className.substring(className.lastIndexOf(".")+1);
 
Element child = XmlUtil.getFirstNamedChild(element, className);
 
Element child = XmlUtil.getFirstNamedChild(element, className);
 
if (child != null) {
 
if (child != null) {
 +
//In this example, we get the cohort name from the configuration element.
 
cohortName = child.getAttribute("cohortName");
 
cohortName = child.getAttribute("cohortName");
 
}
 
}
Line 61: Line 59:
 
/**
 
/**
 
* Get the name of the cohort to which the current object belongs.
 
* Get the name of the cohort to which the current object belongs.
* @param currentObject the object that has madve it down the pipeline
+
* @param currentObject the object that has made it down the pipeline
 
* to the calling stage.
 
* to the calling stage.
 
* @param cachedObject the object that was cached at the head end of the pipe.
 
* @param cachedObject the object that was cached at the head end of the pipe.
Line 68: Line 66:
 
public String getCohortName(DicomObject currentObject, DicomObject cachedObject) {
 
public String getCohortName(DicomObject currentObject, DicomObject cachedObject) {
 
//In this example, we will return the cohortName attribute value  
 
//In this example, we will return the cohortName attribute value  
//obtained from the configuration element
+
//obtained from the configuration element. In more complex situations,
 +
//you may have to construct it by interrogating either or both of the
 +
//objects provided in the method arguments.
 
return cohortName;
 
return cohortName;
 
}
 
}
Line 87: Line 87:
 
/**
 
/**
 
* Disconnect from the external logging database.
 
* Disconnect from the external logging database.
* This method is called when the exporter empties the export queue
+
* This method is called after the exporter empties the export queue
 
* or when the pipeline tells the stage to shut down.
 
* or when the pipeline tells the stage to shut down.
 
* This method should commit the database and then disconnect.
 
* This method should commit the database and then disconnect.
Line 105: Line 105:
 
public synchronized Status export(QueueEntry queueEntry) {
 
public synchronized Status export(QueueEntry queueEntry) {
 
logger.info("Cohort name: "+queueEntry.cohortName);
 
logger.info("Cohort name: "+queueEntry.cohortName);
 +
logger.info("File: "+queueEntry.file.getAbsolutePath());
 
LinkedList<LoggedElement> list = queueEntry.list;
 
LinkedList<LoggedElement> list = queueEntry.list;
 
for (LoggedElement el : list) {
 
for (LoggedElement el : list) {
logger.info(el.getElementTag() + ": " + el.name + ": \"" + el.currentValue + "\"/\"" + el.cachedValue + "\"");
+
logger.info(el.getElementTag() + ": "  
 +
+ el.name + ": "
 +
+ "\"" + el.currentValue + "\""
 +
+ "/"
 +
+ "\"" + el.cachedValue + "\"");
 
}
 
}
 
return Status.OK;
 
return Status.OK;
Line 115: Line 120:
 
</pre>
 
</pre>
  
Here is the <b><tt>ant</tt></b> build file to build it:
+
Here is the <b><tt>ant</tt></b> build file to build it. It assumes a development environment like that described in [[Setting Up a MIRC Development Environment]].
 
<pre>
 
<pre>
 
<project name="ExampleLogAdapter" default="all" basedir=".">
 
<project name="ExampleLogAdapter" default="all" basedir=".">
Line 193: Line 198:
  
 
</pre>
 
</pre>
Here is the <b><tt>ConfigurationTemplates.xml</tt></b> file that connects the plugin into the Launcher program's configuration editor, making it easy to insert the plugin into a configuration. Put this file in the <tt>resources</tt> directory so it will be included in the root of the jar file.
+
Here is the <b><tt>ConfigurationTemplates.xml</tt></b> file that connects the plugin into the Launcher program's configuration editor, making it easy to configure the LogAdapter as a child element of the DicomDifferenceLogger's configuration element. Put this file in the <tt>source/resources</tt> directory so it will be included in the root of the jar file.
 
<pre>
 
<pre>
 
<TemplateDefinitions>
 
<TemplateDefinitions>
Line 216: Line 221:
  
 
</pre>
 
</pre>
Here is an example CTP configuration file element deploying the plugin in a DicomDifferenceLogger stage:
+
Here is an example CTP configuration file element deploying the ExampleLogAdapter in a DicomDifferenceLogger stage:
 
<pre>
 
<pre>
 
<DicomDifferenceLogger
 
<DicomDifferenceLogger

Latest revision as of 12:47, 6 February 2016

This article describes how to develop a LogAdapter to connect the DicomDifferenceLogger pipeline stage to an external database for the purpose of creating a map between PHI and anonymized values in DicomObjects. The intended audience for this article is software engineers who are supporting clinical trials.

As described in Building an Extension JAR, the best way to deploy CTP components is to put them in jars that are placed in the CTP/libraries directory or any of its subdirectories.

1 The LogAdapter Interface

To be recognized as a LogAdapter, a class must implement the org.rsna.ctp.stdstages.logger.LogAdapter interface. The interface provides four methods for interfacing to the external database.

The LogAdapter has a getCohortName method that is passed two DicomObjects, the one that was cached before it passed down the pipeline and the one that was received by the DicomDifferenceLogger stage. If all objects belong to the same cohort, the method can obtain the cohort name from the configuration file element that was passed to the constructor. If not, then the method can interrogate elements in the two objects to determine to which cohort the objects belong.

The LogAdapter has an export method that is passed an org.rsna.ctp.stdstages.logger.QueueEntry object. The QueueEntry object contains the name of the cohort to which the log entry is to be made and a LinkedList of org.rsna.ctp.stdstages.logger.LoggedElement objects, one object for each DICOM element to be logged. Each LoggedElement object contains the name and tag of the element and the value of the element both before and after processing by the pipeline of which the DicomDifferenceLogger is a part.

2 An Example LogAdapter

Here is the code of a test LogAdapter:

package org.rsna.test;

import java.util.LinkedList;
import org.apache.log4j.Logger;
import org.rsna.ctp.objects.DicomObject;
import org.rsna.ctp.pipeline.Status;
import org.rsna.ctp.pipeline.Status;
import org.rsna.ctp.stdstages.logger.*;
import org.rsna.util.XmlUtil;
import org.w3c.dom.Element;

/**
 * An example LogAdapter implementation.
 */
public class ExampleLogAdapter implements LogAdapter {
	
	static final Logger logger = Logger.getLogger(ExampleLogAdapter.class);
	Element element = null;
	String cohortName = "cohort";
	
	/**
	 * Instantiate the LogAdapter.
	 * @param element the XML element from the configuration file
	 * specifying the configuration of the pipeline stage that
	 * will use this LogAdapter to export differences in DicomObjects.
	 * Any configuration information must be contained in attributes
	 * of that stage's element.
	 */
	public ExampleLogAdapter(Element element) {
		this.element = element;
		
		//If you need to capture anything from the configuration element,
		//do it here. Note: the configuration element of the LogAdapter is a child
		//of element. By convention, the name of the child element is the name of
		//the LogAdapter class (in this case, ExampleLogAdapter). 
		String className = getClass().getName();
		className = className.substring(className.lastIndexOf(".")+1);
		Element child = XmlUtil.getFirstNamedChild(element, className);
		if (child != null) {
			//In this example, we get the cohort name from the configuration element.
			cohortName = child.getAttribute("cohortName");
		}
	}

	/**
	 * Get the name of the cohort to which the current object belongs.
	 * @param currentObject the object that has made it down the pipeline
	 * to the calling stage.
	 * @param cachedObject the object that was cached at the head end of the pipe.
	 * @return the name of the cohort to which the objects belong.
	 */
	public String getCohortName(DicomObject currentObject, DicomObject cachedObject) {
		//In this example, we will return the cohortName attribute value 
		//obtained from the configuration element. In more complex situations,
		//you may have to construct it by interrogating either or both of the
		//objects provided in the method arguments.
		return cohortName;
	}

	/**
	 * Connect to the external logging database.
	 * This method is called whenever the exporter starts a sequence of exports.
	 * It is not called again until the exporter empties the export queue,
	 * disconnects, and then receives another QueueEntry to export.
	 * @return Status.OK or Status.FAIL
	 */
	public synchronized Status connect() {
		//Add code here to connect to the external database
		//...
		return Status.OK;
	}
	
	/**
	 * Disconnect from the external logging database.
	 * This method is called after the exporter empties the export queue
	 * or when the pipeline tells the stage to shut down.
	 * This method should commit the database and then disconnect.
	 * @return Status.OK or Status.FAIL
	 */
	public synchronized Status disconnect() {
		//Add code here to commit and disconnect.
		//...
		return Status.OK;
	}
	
	/**
	 * Export one QueueEntry to the external logging database.
	 * This method is called from the exporter thread.
	 * @return Status.OK, Status.RETRY, or Status.FAIL
	 */
	public synchronized Status export(QueueEntry queueEntry) {
		logger.info("Cohort name: "+queueEntry.cohortName);
		logger.info("File: "+queueEntry.file.getAbsolutePath());
		LinkedList<LoggedElement> list = queueEntry.list;
		for (LoggedElement el : list) {
			logger.info(el.getElementTag() + ": " 
						+ el.name + ": "
						+ "\"" + el.currentValue + "\""
						+ "/"
						+ "\"" + el.cachedValue + "\"");
		}
		return Status.OK;
	}
	
}

Here is the ant build file to build it. It assumes a development environment like that described in Setting Up a MIRC Development Environment.

<project name="ExampleLogAdapter" default="all" basedir=".">

	<property name="build" value="${basedir}/build"/>
	<property name="libraries" value="${basedir}/libraries"/>
	<property name="products" value="${basedir}/products"/>
	<property name="source" value="${basedir}/source"/>
	<property name="java" value="${source}/java"/>
	<property name="resources" value="${source}/resources"/>
	<property name="ctp" value="D:/Development/CTP"/>

	<path id="classpath">
		<pathelement location="${libraries}/util.jar"/>
		<pathelement location="${libraries}/CTP.jar"/>
		<pathelement location="${libraries}/log4j.jar"/>
	</path>

	<target name="clean">
		<delete dir="${build}" failonerror="false"/>
	</target>

	<target name="init">
		<mkdir dir="${build}"/>
		<tstamp>
			<format property="today" pattern="yyyy.MM.dd"/>
			<format property="now" pattern="HH:mm:ss"/>
		</tstamp>
		<echo message="Time now ${now}"/>
		<echo message="ant.java.version = ${ant.java.version}" />
		<delete dir="${products}" failonerror="false" />
		<mkdir dir="${build}"/>
		<mkdir dir="${products}"/>
	</target>

	<target name="getLibraryJars">
		<copy overwrite="true" todir="${libraries}">
			<fileset dir="${ctp}/libraries">
				<include name="CTP.jar"/>
				<include name="util.jar"/>
				<include name="log4j.jar"/>
			</fileset>
		</copy>
	</target>

	<target name="jar" depends="init, getLibraryJars">

		<javac destdir="${build}" optimize="on"
				classpathref="classpath"
				includeantruntime="false"
				debug="true" debuglevel="lines,vars,source">
			<src path="${java}"/>
		</javac>

		<copy overwrite="true" todir="${build}">
			<fileset dir="${resources}"/>
		</copy>

		<jar jarfile="${products}/ExampleLogAdapter.jar">
			<manifest>
				<attribute name="Implementation-Version" value="${today} @ ${now}"/>
			</manifest>
			<fileset dir="${build}" includes="**"/>
		</jar>

	</target>
	
	<target name="deploy">
		<copy overwrite="true" todir="D:/JavaPrograms/CTP-LoggerTest/CTP/libraries">
			<fileset dir="${products}"/>
		</copy>		
	</target>

	<target name="all" depends="clean, jar, deploy"/>

</project>

Here is the ConfigurationTemplates.xml file that connects the plugin into the Launcher program's configuration editor, making it easy to configure the LogAdapter as a child element of the DicomDifferenceLogger's configuration element. Put this file in the source/resources directory so it will be included in the root of the jar file.

<TemplateDefinitions>

	<Components>
	
		<AttachedChild>
			<parent class="org.rsna.ctp.stdstages.DicomDifferenceLogger"/>
	
			<child name="ExampleLogAdapter" required="yes" allowMultiples="no">
				<attr name="cohortName" required="yes" default="Cohort">
					<helptext>
						The name of the cohort.
					</helptext>
				</attr>
			</child>
		</AttachedChild>
	
	</Components>

</TemplateDefinitions>

Here is an example CTP configuration file element deploying the ExampleLogAdapter in a DicomDifferenceLogger stage:

<DicomDifferenceLogger
    adapterClass="org.rsna.test.ExampleLogAdapter"
    cacheID="ObjectCache"
    class="org.rsna.ctp.stdstages.DicomDifferenceLogger"
    name="DicomDifferenceLogger"
    quarantine="quarantines/DicomDifferenceLogger"
    root="roots/DicomDifferenceLogger"
    tags="PatientName;PatientID">

    <ExampleLogAdapter cohortName="TheCohortName"/>

</DicomDifferenceLogger>

All the code of the ExampleLogAdapter is available at https://github.com/johnperry/ExampleLogAdapter.