LAUREL BRIDGE

LaurelBridge.DCF.Examples.QuerySCP Namespace

DICOM Connectivity Framework V3.4
The QuerySCP example demonstrates how to use DCF to implement the DICOM requirements of a query service class provider.
Classes

  ClassDescription
Public classCallbackQueryServer
A class that demonstrates the callback style server for a QR SCP.
Public classExtendedQRSCP
The ExtendedQRSCP class provides an alternate implementation of the QRSCP that overrides the DimseServiceUser methods directly, instead of using the callback techniques used in the CallbackQueryServer.
Public classProgram
Basic query service class provider example which should be run with the QuerySCU.
Examples

QuerySCP Sample Code
public class Program
{
    /// <summary>
    /// Main entry point for QuerySCP.
    /// </summary>
    public static void Main()
    {
        try
        {
            AllowedPresentationContext cFindContext = new AllowedPresentationContext(
                Uids.StudyRootQueryRetrieveInformationModelFIND,
                new List<string> { Uids.TransferSyntax.ExplicitVRLittleEndian, Uids.TransferSyntax.ImplicitVRLittleEndian });
            AllowedPresentationContext cMoveContext = new AllowedPresentationContext(
                Uids.StudyRootQueryRetrieveInformationModelMOVE,
                new List<string> { Uids.TransferSyntax.ExplicitVRLittleEndian, Uids.TransferSyntax.ImplicitVRLittleEndian });
            AllowedPresentationContext cGetContext = new AllowedPresentationContext(
                Uids.StudyRootQueryRetrieveInformationModelGET,
                new List<string> { Uids.TransferSyntax.ExplicitVRLittleEndian, Uids.TransferSyntax.ImplicitVRLittleEndian });
            AllowedPresentationContext mrStore = new AllowedPresentationContext(
                Uids.MRImageStorage,
                new List<string> { Uids.ILE });
            CallbackQueryServer queryServer = new CallbackQueryServer(
                new List<AllowedPresentationContext> { cFindContext, cMoveContext, cGetContext, mrStore });

            AssociationManager mgr = new AssociationManager();
            mgr.ServerTcpPort = 104;
            mgr.AssociationConfigPolicyMgr = queryServer;
            mgr.AddAssociationListener(queryServer);

            // AssociationManager.run() blocks, so start him listening for connections on a separate thread
            Thread t = new Thread(mgr.Run);
            t.Start();
            if (!mgr.WaitForRunning(2000))
            {
                throw new TimeoutException("AssociationManager did not start in an acceptable amount of time");
            }

            Console.WriteLine("listening on {0}...", 104);
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception caught during execution: {0}", e);
        }
    }
}

/// <summary>
/// A class that demonstrates the callback style server for a QR SCP.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="IAssociationConfigPolicyManager"/> allows us to get a callback to set our
/// session settings for the association.
/// </para>
/// <para>
/// This class overrides the <see cref="AssociationListenerAdapter.BeginAssociation"/> method
/// to install a QRSCP.  If UseCallbackServer is true, a stock <see cref="QRSCP"/>
/// is registered as the provider with callbacks that delegate to members of this class.
/// If UseCallbackServer is false, the <see cref="ExtendedQRSCP"/> class that
/// extends <see cref="QRSCP"/> is registered as the provider and overrides the DimseServiceUser
/// methods.
/// </para>
/// <para>
/// Other <see cref="AssociationListenerAdapter"/> methods may be overridden to implement
/// additional functionality.
/// </para>
/// </remarks>
public class CallbackQueryServer : AssociationListenerAdapter, IAssociationConfigPolicyManager
{
    readonly IList<AllowedPresentationContext> _presentationContexts;

    /// <summary>
    /// Change this to true if you would prefer to use the callback-style server implemented in this class,
    /// otherwise the ExtendedQRSCP class will be used.  See the <see cref="BeginAssociation"/> override.
    /// </summary>
    private readonly bool UseCallbackServer = false;

    private bool CancelRequested { get; set; }

    /// <summary>
    /// Constructs an CallbackStoreServer will service incoming associations.
    /// </summary>
    /// <param name="allowedPresentationContexts">The list of presentation contexts that will be allowed</param>
    public CallbackQueryServer(IList<AllowedPresentationContext> allowedPresentationContexts)
    {
        _presentationContexts = allowedPresentationContexts;
    }

    #region IAssociationConfigPolicyManager Overrides
    /// <summary>
    /// Returns a DicomSessionSettings object to be used for the association.
    /// </summary>
    /// <param name="assoc">The AssociationAcceptor for the given association</param>
    /// <returns>The DicomSessionSettings for the given association.</returns>
    public DicomSessionSettings GetSessionSettings(AssociationAcceptor assoc)
    {
        return new DicomSessionSettings() { SessionName = String.Format("QRServer: {0}", assoc.DicomSocket.ConnectionID) };
    }
    #endregion

    #region AssociationListener Overrides
    /// <summary>
    /// Creates the QRSCP to handle the association that caused this method to be called.
    /// </summary>
    /// <param name="assoc">The AssociationAcceptor for the given association</param>
    public override void BeginAssociation(AssociationAcceptor assoc)
    {
        assoc.RegisterServiceClassProvider(UseCallbackServer
            ? new QRSCP(assoc, _presentationContexts, CFind, CMove, CGet, CCancel)
            : new ExtendedQRSCP(assoc, _presentationContexts));
    }
    #endregion

    #region DimseServiceUser Overrides
    /// <summary>
    /// Create and send some canned responses to the SCU.
    /// </summary>
    /// <param name="acceptor">The AssociationAcceptor for the given association</param>
    /// <param name="request">The inbound CFindRequest</param>
    /// <returns>An IEnumerable list of zero or more pending responses and one final response</returns>
    public IEnumerable<CFindResponse> CFind(AssociationAcceptor acceptor, CFindRequest request)
    {
        Console.WriteLine("CallbackQueryServer.CFind: {0} to port {1}:{2}{3}",
            acceptor.AssociationInfo.CallingTitle,
            acceptor.AssociationInfo.CalledPresentationAddress,
            Environment.NewLine,
            request.Data);

        foreach (DicomDataSet result in FindMatchingDataSets(request.Data))
        {
            if (CancelRequested)
                break;

            CFindPendingResponse pending = new CFindPendingResponse(request, result);
            Console.WriteLine("CallbackQueryServer: sending C-Find pending response dataset:{0}{1}", Environment.NewLine, pending);
            yield return pending;
        }

        CFindFinalResponse finalResponse = new CFindFinalResponse(request);
        if (CancelRequested)
            finalResponse.Status = DimseStatus.CANCEL;

        Console.WriteLine("CallbackQueryServer: sending the C-Find final response dataset:{0}{1}", Environment.NewLine, finalResponse);
        yield return finalResponse;
    }

    /// <summary>
    /// Implementation of the CCancel handler.
    /// </summary>
    /// <param name="acceptor">The Association acceptor.</param>
    /// <param name="request">The CCancelRequest DIMSE message.</param>
    public void CCancel(AssociationAcceptor acceptor, CCancelRequest request)
    {
        Console.WriteLine("CallbackQueryServer.CCancel: got a cancel request from {0} {1}",
            acceptor.AssociationInfo.CallingTitle,
            acceptor.AssociationInfo.CalledPresentationAddress);
        CancelRequested = true;
    }

    /// <summary>
    /// Move some canned responses to a Store SCP and send CMoveResponses the SCU.
    /// </summary>
    /// <param name="acceptor">The AssociationAcceptor for the given association</param>
    /// <param name="request">The inbound CMoveRequest</param>
    /// <returns>An IEnumerable list of zero or more pending responses and one final response</returns>
    public IEnumerable<CMoveResponse> CMove(AssociationAcceptor acceptor, CMoveRequest request)
    {
        Console.WriteLine("CallbackQueryServer.CMove: {0} to port {1}:{2}{3}",
            acceptor.AssociationInfo.CallingTitle,
            acceptor.AssociationInfo.CalledPresentationAddress,
            Environment.NewLine,
            request.Data);

        AssociationInfo ainfo = new AssociationInfo();
        ainfo.CalledTitle = request.MoveDestination;
        ainfo.CallingTitle = acceptor.AssociationInfo.CalledTitle;
        ainfo.CalledPresentationAddress = "localhost:105";

        byte ctxId = 1;
        IList<DicomDataSet> dataSets = new List<DicomDataSet>();
        foreach (DicomDataSet dds in FindMatchingDataSets(request.Data))
        {
            if (CancelRequested)
                break;

            bool foundContext = false;
            foreach (RequestedPresentationContext ctx in ainfo.RequestedPresentationContextList)
            {
                if (dds.GetElementStringValue(Tags.SOPClassUID).Equals(ctx.AbstractSyntax))
                {
                    foundContext = true;
                    break;
                }
            }

            if (!foundContext)
            {
                ainfo.AddRequestedPresentationContext(
                    new RequestedPresentationContext(
                        ctxId,
                        dds.GetElementStringValue(Tags.SOPClassUID),
                        dds.GetElementStringValue(Tags.TransferSyntaxUID)));
                ctxId += 2;
            }

            dataSets.Add(dds);
        }

        // The SubOps class handles gathering DIMSE status for the CStore responses we will receive
        // so that we may return a correct final response to the QRSCU.
        SubOps counts = new SubOps();
        counts.RemainingSubOps = dataSets.Count;

        StoreSCU scu = new StoreSCU(ainfo, acceptor.SessionSettings);
        scu.RequestAssociation();
        foreach (DicomDataSet dds in dataSets)
        {
            if (CancelRequested)
                break;

            DimseMessage dimseResponse = scu.CStore(dds, acceptor.SessionSettings.SendDimseTimeoutSeconds);
            counts.Update(dimseResponse.Status, dds.GetElementStringValue(Tags.SOPInstanceUID));

            CMoveResponse response = new CMovePendingResponse(request);
            response.UpdateSubOps(counts);
            yield return response;
        }
        scu.ReleaseAssociation();

        CMoveFinalResponse finalResponse = new CMoveFinalResponse(request);
        finalResponse.UpdateSubOps(counts);
        if (CancelRequested)
            finalResponse.SetCancelled();

        yield return finalResponse;
    }

    /// <summary>
    /// Implementation of the CGet DIMSE handler.
    /// </summary>
    /// <param name="acceptor">The AssociationAcceptor for this request.</param>
    /// <param name="request">The CGet DIMSE message.</param>
    /// <returns>An enumeration of CGetResponse messages.</returns>
    public IEnumerable<CGetResponse> CGet(AssociationAcceptor acceptor, CGetRequest request)
    {
        Console.WriteLine("CallbackQueryServer.CGet: {0} to port {1}:{2}{3}",
            acceptor.AssociationInfo.CallingTitle, acceptor.AssociationInfo.CalledPresentationAddress,
            Environment.NewLine,
            request.Data);

        IList<DicomDataSet> dataSets = new List<DicomDataSet>();
        foreach (DicomDataSet dds in FindMatchingDataSets(request.Data))
        {
            dataSets.Add(dds);
        }

        // The SubOps class handles gathering DIMSE status for the CStore responses we will receive
        // so that we may return a correct final response to the QRSCU.
        SubOps counts = new SubOps();
        counts.RemainingSubOps = dataSets.Count;
        foreach (DicomDataSet dds in dataSets)
        {
            CStoreRequest storeRequest = new CStoreRequest();
            storeRequest.Data = dds;
            acceptor.SendDimseMessage(storeRequest, acceptor.SessionSettings.SendDimseTimeoutSeconds);
            DimseMessage dimseResponse = acceptor.ReceiveDimseMessage(0, acceptor.SessionSettings.ReceiveDimseTimeoutSeconds);
            counts.Update(dimseResponse.Status, dds.GetElementStringValue(Tags.SOPInstanceUID));
            CGetResponse response = new CGetPendingResponse(request);
            response.UpdateSubOps(counts);
            yield return response;
        }

        yield return new CGetFinalResponse(request);
    }
    #endregion

    #region Application Overrides
    /// <summary>
    /// This method is where you would find your matching datasets based upon the query request.
    /// </summary>
    /// <remarks>
    /// A QueryServer will have a backing database or service that provides the information that
    /// is returned by the server.
    /// </remarks>
    /// <param name="query">The CFind query request.</param>
    /// <returns>An enumeration of DicomDataSet objects that match the query.</returns>
    private IEnumerable<DicomDataSet> FindMatchingDataSets(DicomDataSet query)
    {
        if (query == null)
            throw new ArgumentNullException("query");
        // TODO: replace with code to find matching datasets using the query dataset
        string location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".";
        foreach (string fileName in Directory.GetFiles(location, "*.dcm"))
        {
            using (DicomFileInput dfi = new DicomFileInput(fileName))
            {
                DicomDataSet dds = dfi.ReadDataSet();
                // since we are forcing the DicomFileInput to close (ala using) expand streaming mode data here
                dds.ExpandStreamingModeData(true);

                // Use the DicomDataSet Compare method in Query Mode (2) 
                if (dds.Compare(query, 2))
                {
                    yield return dds;
                }
            }
        }
    }
    #endregion
}