Message routing

The DICOM routing functionality consists of:

  • Association Mode (Handled by Dcmtk)
    • DICOM Protocol accepts only messages that match abstract syntax in NetConfig. This is checked at association context since abstract syntax is only available at this time.
    • Association context is stored in DICOM Session.
    • Command code is set to "0". This indicates "association context" for the purposes of forming TRXID.
    • SCU sends a set of transfer syntax. Cloverleaf intersects this set from the set that is present in NetConfig, only forwarding the remaining.
    • Message is routed according to TrxID (= Abstract Syntax + Command Code).
    • One presentation context is accepted by SCP. This presentation ID is saved in the DICOM session and is used to look up "Abstract Syntax during Data Exchange Context".
  • Data Exchange Mode
    • Each PDU contains the presentation context ID. This is used to look up Abstract Context.
    • SCU can send multiple PDUs for a particular message. Cloverleaf collects all of them at the inbound and creates a SOP (=DIMSE + IOD). The remaining operations start only when all the PDUs for that message have arrived at the protocol and SOP is successfully created.
    • Cloverleaf looks up Abstract Context from the DICOM Session by the presentation context ID that is in the PDUs. It also looks up the command code from the DIMSE. Abstract Context + Command code = TrxID.
    • Message is routed according to TrxID.
    • At the outbound, SOP is broken into individual PDUs and sent out, keeping the SCP’s PDU size limits under consideration.
  • Message Reply:
    • In Phase 1, only replies from Called AET are passed through. All other replies are dropped that are not coming from Called AET.
    • Static Route is used as a route reply.
    • UPOC is used to create Reply based on Request.

Tcl example

proc ack_dicom { args } {
 global HciConnName ;# Name of thread 
 keylget args MODE mode ;# Fetch mode
 set ctx "" ; keylget args CONTEXT ctx ;# Fetch tps caller context
 set uargs {} ; keylget args ARGS uargs ;# Fetch user-supplied args
 set debug 0 ; ;# Fetch user argument DEBUG and
 catch {keylget uargs DEBUG debug} ;# assume uargs is a keyed list
 set module "ack_dicom/$HciConnName/$ctx" ;# Use this before every echo/puts,
 ;# it describes where the text came from
 set dispList {} ;# Nothing to return
 switch -exact -- $mode {
 start {
 # Perform special init functions
 # N.B.: there may or may not be a MSGID key in args 
 if { $debug } {
 puts stdout "$module: Starting in debug mode..."
 }
 }
 run {
 # 'run' mode always has a MSGID; fetch and process it
 keylget args MSGID mh
 msgmetaset $mh DRIVERCTL "{TransferSyntax 1.2.840.10008.1.2.4.91}"
 set gh [grmcreate -msg $mh ldl 2014 test C_STORE]
 set gh_reply [grmcreate ldl 2014 test C_STORE_RSP]
 # Get fields
 set field0 [grmfetch $gh 0(0).C_STORE_RQ_DIMSE(0).0000,0000(0)]
 set field2 [grmfetch $gh 0(0).C_STORE_RQ_DIMSE(0).0000,0002(0)]
 set field120 [grmfetch $gh 0(0).C_STORE_RQ_DIMSE(0).0000,0110(0)]
 set field1000 [grmfetch $gh 0(0).C_STORE_RQ_DIMSE(0).0000,1000(0)]
 # Get DATA
 set COMMANDGROUP [datget $field0 VALUE]
 set SOPCLASSUID [datget $field2 VALUE]
 set MESSAGEID [datget $field120 VALUE]
 set AFSOPINSTANCEUID [datget $field1000 VALUE]
 puts $COMMANDGROUP
 puts $SOPCLASSUID
 puts $MESSAGEID
 puts $AFSOPINSTANCEUID 
 # Store DATA
 grmstore $gh_reply 0(0).C_STORE_RSP_DIMSE(0).0000,0000(0) c $COMMANDGROUP
 grmstore $gh_reply 0(0).C_STORE_RSP_DIMSE(0).0000,0002(0) c $SOPCLASSUID
 grmstore $gh_reply 0(0).C_STORE_RSP_DIMSE(0).0000,0100(0) c "32789"
 grmstore $gh_reply 0(0).C_STORE_RSP_DIMSE(0).0000,0120(0) c $MESSAGEID
 grmstore $gh_reply 0(0).C_STORE_RSP_DIMSE(0).0000,0800(0) c "257"
 grmstore $gh_reply 0(0).C_STORE_RSP_DIMSE(0).0000,0900(0) c "0"
 grmstore $gh_reply 0(0).C_STORE_RSP_DIMSE(0).0000,1000(0) c $AFSOPINSTANCEUID 
 set msg_new [grmencode -meta {DRIVERCTL {{TransferSyntax 1.2.840.10008.1.2}}} $gh_reply] 
 grmdestroy $gh
 grmdestroy $gh_reply
 lappend dispList "SEND $msg_new" "CONTINUE $mh"
 }
 time {
 # Timer-based processing
 # N.B.: there may or may not be a MSGID key in args 
 }
 shutdown {
 # Doing some clean-up work 
 } 
 default {
 error "Unknown mode '$mode' in $module"
 }
 }
 return $dispList
}

Java UPoC example

package hc.cloverleaf.dicom;
import hc.cloverleaf.Logger;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

import org.apache.commons.codec.binary.Hex;
import org.dcm4che2.data.DicomObject;
import org.dcm4che2.io.DicomInputStream;
import org.dcm4che2.io.DicomOutputStream;
import org.dcm4che2.net.CommandUtils;

import com.quovadx.cloverleaf.upoc.CloverEnv;
import com.quovadx.cloverleaf.upoc.CloverleafException;
import com.quovadx.cloverleaf.upoc.DispositionList;
import com.quovadx.cloverleaf.upoc.Message;
import com.quovadx.cloverleaf.upoc.PropertyTree;
public class HC_dicomACK extends com.quovadx.cloverleaf.upoc.TPS {
	private static int loglevel = 3;
	private static final Logger logger = new Logger ();
	String siteDir = "";
	String rootDir = "";

	/*
	 * Constructor using <code>Upoc.extractAndValidateUserArguments</code> to
	 * populate private member fields generated from User Code Component definition.
	 */
	public HC_dicomACK (CloverEnv cloverEnv, PropertyTree xArgs)throws CloverleafException
	{
		super(cloverEnv,xArgs);
		extractAndValidateUserArguments(cloverEnv, xArgs);
		siteDir = cloverEnv.getSiteDirName();
		rootDir = cloverEnv.getRootName();
		logger.setLoglevel(3);
	}
	/**
	 * The <code>process</code> method is called by the Cloverleaf engine at a
	 * defined point of control, to allow user creation of messages and/or control
	 * and modification of the content and the flow of a message passing through
	 * the engine.
	 * @param cloverEnv an opaque reference to the Cloverleaf run-time environment
	 * @param context   identifies which TPS-style Point of Control is involved
	 * @param mode      start | run | time (time is only used in protocol read TPS's)
	 * @param msg       reference to message being processed.  This will be
	 *                  <code>null</code> for start or time mode, unless there is
	 *                  more than one TPS on the stack and an earlier one has
	 *                  produced a message.
	 * @return DispositionList - instructs the engine what to do with any message
	 *  passed into the call, and any message(s) generated within the call.
	 */
	public DispositionList process (
			CloverEnv cloverEnv,
			String context,
			String mode,
			com.quovadx.cloverleaf.upoc.Message msg)
	throws CloverleafException {

		// Initialize our return value
		DispositionList dispList = new DispositionList();
		logger.setLoglevel(3);

		InputStream is = null;
		DicomInputStream din = null;
		String transferSyntax = "";
		DicomObject dob = null;
		
		Message ack = null;

		if (msg != null) {
			logger.log("Processing message...",3);
			try {
				is = new ByteArrayInputStream(msg.getBytes());
				//is = new ByteArrayInputStream(msg.getContent().getBytes("ASCII"));
			} catch (CloverleafException e2) {
				e2.printStackTrace();
			}
			logger.log("Message is:\n" +  Hex.encodeHexString(msg.getBytes()), 3);
			try { 
				din = new DicomInputStream(is,msg.metadata.getDriverctl().getString("TransferSyntax"));
				try {
					if (din.getDicomObject()==null) {logger.log("no DicomObject", 3);}
					dob = din.readDicomObject();
					logger.log("DATA MESSAGE DICOM Object = \n"+dob.toString(),3);
				} catch (Exception e1) {
					logger.log("failed",3);e1.printStackTrace();
				}
			}
			catch (Exception e) {e.printStackTrace();}
			finally {
				try {
					is.close();
				}
				catch (IOException ignore) {
				}
			} 
			DicomObject response = CommandUtils.mkRSP(dob, CommandUtils.SUCCESS);
			//logger.log("RESPONSE DICOM Object = \n"+response.toString(),3);
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			DicomOutputStream dos = new DicomOutputStream(baos);
			try {			
				dos.writeDicomObject(response, din.getTransferSyntax());
				ack = msg.copy();
				ack.setBytes(baos.toByteArray()); 
				//String meta = "{CallingAET "+sourceAETitle+"} {CalledAET CLOVERLEAF} {AbstractSyntax "+sopClassUID+"} {TransferSyntax "+transferSyntax+"}";
				logger.log("Response is:\n" +  Hex.encodeHexString( ack.getBytes()), 3);
				
				is = new ByteArrayInputStream(ack.getBytes());
				din = new DicomInputStream(is,ack.metadata.getDriverctl().getString("TransferSyntax"));	
				dob = din.readDicomObject();
				logger.log("REPLY MESSAGE DICOM Object = \n"+dob.toString(),3);
			}
			catch (IOException e) {
				e.printStackTrace();
				return dispList;
			} 
			finally {
				try {
					dos.close();
				}
				catch (IOException ignore) {
				}
			}
			//Return our disposition list
			dispList.add(DispositionList.OVER, ack);
			dispList.add(DispositionList.CONTINUE, msg);
			logger.log("Done. Continuing message...",3);
		}
		return dispList;
	}
}