Developing a DicomDifferenceLogger LogAdapter

From MircWiki
Jump to navigation Jump to search

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.