view OrthancServer/Sources/OrthancGetRequestHandler.cpp @ 5911:bfae0fc2ea1b get-scu-test

Started to work on handling errors as warnings when trying to store instances whose SOPClassUID has not been accepted during the negotiation. Work to be finalized later
author Alain Mazy <am@orthanc.team>
date Mon, 09 Dec 2024 10:07:19 +0100
parents dfd5effec064
children
line wrap: on
line source

/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/

#include "PrecompiledHeadersServer.h"
#include "OrthancGetRequestHandler.h"

#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
#include "../../OrthancFramework/Sources/Logging.h"
#include "../../OrthancFramework/Sources/MetricsRegistry.h"
#include "OrthancConfiguration.h"
#include "ServerContext.h"
#include "ServerJobs/DicomModalityStoreJob.h"

#include <dcmtk/dcmdata/dcdeftag.h>
#include <dcmtk/dcmdata/dcfilefo.h>
#include <dcmtk/dcmdata/dcistrmb.h>
#include <dcmtk/dcmnet/assoc.h>
#include <dcmtk/dcmnet/dimse.h>
#include <dcmtk/dcmnet/diutil.h>
#include <dcmtk/ofstd/ofstring.h>

#include <sstream>  // For std::stringstream

namespace Orthanc
{
  static void ProgressCallback(void *callbackData,
                               T_DIMSE_StoreProgress *progress,
                               T_DIMSE_C_StoreRQ *req)
  {
    if (req != NULL &&
        progress->state == DIMSE_StoreBegin)
    {
      OFString str;
      CLOG(TRACE, DICOM) << "Sending Store Request following a C-GET:" << std::endl
                         << DIMSE_dumpMessage(str, *req, DIMSE_OUTGOING);
    }
  }


  bool OrthancGetRequestHandler::DoNext(T_ASC_Association* assoc)
  {
    if (position_ >= instances_.size())
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
    
    const std::string& id = instances_[position_++];

    std::string dicom;
    context_.ReadDicom(dicom, id);
    
    if (dicom.empty())
    {
      throw OrthancException(ErrorCode_BadFileFormat);
    }

    std::unique_ptr<DcmFileFormat> parsed(
      FromDcmtkBridge::LoadFromMemoryBuffer(dicom.c_str(), dicom.size()));

    if (parsed.get() == NULL ||
        parsed->getDataset() == NULL)
    {
      throw OrthancException(ErrorCode_InternalError);
    }
    
    DcmDataset& dataset = *parsed->getDataset();
    
    OFString a, b;
    if (!dataset.findAndGetOFString(DCM_SOPClassUID, a).good() ||
        !dataset.findAndGetOFString(DCM_SOPInstanceUID, b).good())
    {
      throw OrthancException(ErrorCode_NoSopClassOrInstance,
                             "Unable to determine the SOP class/instance for C-STORE with AET " +
                             originatorAet_);
    }

    std::string sopClassUid(a.c_str());
    std::string sopInstanceUid(b.c_str());
    
    return PerformGetSubOp(assoc, sopClassUid, sopInstanceUid, parsed.release());
  }

  
  void OrthancGetRequestHandler::AddFailedUIDInstance(const std::string& sopInstance)
  {
    if (failedUIDs_.empty())
    {
      failedUIDs_ = sopInstance;
    }
    else
    {
      failedUIDs_ += "\\" + sopInstance;
    }
  }


  static bool SelectPresentationContext(T_ASC_PresentationContextID& selectedPresentationId,
                                        DicomTransferSyntax& selectedSyntax,
                                        T_ASC_Association* assoc,
                                        const std::string& sopClassUid,
                                        DicomTransferSyntax sourceSyntax,
                                        bool allowTranscoding)
  {
    typedef std::map<DicomTransferSyntax, T_ASC_PresentationContextID> Accepted;

    Accepted accepted;

    /**
     * 1. Inspect and index all the accepted transfer syntaxes. This
     * is similar to the code from "DicomAssociation::Open()".
     **/

    LST_HEAD **l = &assoc->params->DULparams.acceptedPresentationContext;
    if (*l != NULL)
    {
      DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l);
      LST_Position(l, (LST_NODE*)pc);
      while (pc)
      {
        DicomTransferSyntax transferSyntax;
        if (pc->result == ASC_P_ACCEPTANCE)
        {
          if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
          {
            /*CLOG(TRACE, DICOM) << "C-GET SCP accepted: SOP class " << pc->abstractSyntax
              << " with transfer syntax " << GetTransferSyntaxUid(transferSyntax);*/
            if (std::string(pc->abstractSyntax) == sopClassUid)
            {
              accepted[transferSyntax] = pc->presentationContextID;
            }
          }
          else
          {
            CLOG(WARNING, DICOM) << "C-GET: Unknown transfer syntax received: "
                                 << pc->acceptedTransferSyntax;
          }
        }
            
        pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
      }
    }

    
    /**
     * 2. Select the preferred transfer syntaxes, which corresponds to
     * the source transfer syntax, plus all the uncompressed transfer
     * syntaxes if transcoding is enabled.
     * This way, we minimize the transcoding on our side.
     **/
    
    std::list<DicomTransferSyntax> preferred;
    preferred.push_back(sourceSyntax);

    if (allowTranscoding)
    {
      if (sourceSyntax != DicomTransferSyntax_LittleEndianImplicit)
      {
        // Default Transfer Syntax for DICOM
        preferred.push_back(DicomTransferSyntax_LittleEndianImplicit);
      }

      if (sourceSyntax != DicomTransferSyntax_LittleEndianExplicit)
      {
        preferred.push_back(DicomTransferSyntax_LittleEndianExplicit);
      }

      if (sourceSyntax != DicomTransferSyntax_BigEndianExplicit)
      {
        // Retired
        preferred.push_back(DicomTransferSyntax_BigEndianExplicit);
      }
    }


    /**
     * 3. Lookup whether one of the preferred transfer syntaxes was
     * accepted.
     **/
    
    for (std::list<DicomTransferSyntax>::const_iterator
           it = preferred.begin(); it != preferred.end(); ++it)
    {
      Accepted::const_iterator found = accepted.find(*it);
      if (found != accepted.end())
      {
        selectedPresentationId = found->second;
        selectedSyntax = *it;
        return true;
      }
    }

    // No preferred syntax was accepted but, if a PC has been accepted, it means that we have accepted a TS.
    // This maybe means that we need to transcode twice on our side (from a compressed format to another compressed format).
    if (allowTranscoding && accepted.size() >  0)
    {
      Accepted::const_iterator it = accepted.begin();
      selectedPresentationId = it->second;
      selectedSyntax = it->first;
      return true;
    }

    return false;
  }                                                           


  bool OrthancGetRequestHandler::PerformGetSubOp(T_ASC_Association* assoc,
                                                 const std::string& sopClassUid,
                                                 const std::string& sopInstanceUid,
                                                 DcmFileFormat* dicomRaw)
  {
    assert(dicomRaw != NULL);
    std::unique_ptr<DcmFileFormat> dicom(dicomRaw);
    
    DicomTransferSyntax sourceSyntax;
    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *dicom))
    {
      failedCount_++;
      AddFailedUIDInstance(sopInstanceUid);
      throw OrthancException(ErrorCode_NetworkProtocol, 
                             "C-GET SCP: Unknown transfer syntax: (" +
                             std::string(dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT")) +
                             ") " + sopClassUid);
    }

    T_ASC_PresentationContextID presId = 0;  // Unnecessary initialization, makes code clearer
    DicomTransferSyntax selectedSyntax;
    if (!SelectPresentationContext(presId, selectedSyntax, assoc, sopClassUid,
                                   sourceSyntax, allowTranscoding_) ||
        presId == 0)
    {
      failedCount_++;
      AddFailedUIDInstance(sopInstanceUid);
      LOG(WARNING) << "C-GET SCP: storeSCU: No presentation context for: (" 
                   << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
      return true;
    }
    else
    {
      CLOG(INFO, DICOM) << "C-GET SCP selected transfer syntax " << GetTransferSyntaxUid(selectedSyntax)
                        << ", for source instance with SOP class " << sopClassUid
                        << " and transfer syntax " << GetTransferSyntaxUid(sourceSyntax);

      // make sure that we can send images in this presentation context
      T_ASC_PresentationContext pc;
      ASC_findAcceptedPresentationContext(assoc->params, presId, &pc);
      // the acceptedRole is the association requestor role

      if (pc.acceptedRole != ASC_SC_ROLE_DEFAULT &&  // "DEFAULT" is necessary for GinkgoCADx
          pc.acceptedRole != ASC_SC_ROLE_SCP &&
          pc.acceptedRole != ASC_SC_ROLE_SCUSCP)
      {
        // the role is not appropriate
        failedCount_++;
        AddFailedUIDInstance(sopInstanceUid);
        throw OrthancException(ErrorCode_NetworkProtocol,
                               "C-GET SCP: storeSCU: [No presentation context with requestor SCP role for: (" +
                               std::string(dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT")) +
                               ") " + sopClassUid);
      }
    }

    const DIC_US msgId = assoc->nextMsgID++;
    
    T_DIMSE_C_StoreRQ req;
    memset(&req, 0, sizeof(req));
    req.MessageID = msgId;
    strncpy(req.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN);
    strncpy(req.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN);
    req.DataSetType = DIMSE_DATASET_PRESENT;
    req.Priority = DIMSE_PRIORITY_MEDIUM;
    req.opts = 0;
    
    T_DIMSE_C_StoreRSP rsp;
    memset(&rsp, 0, sizeof(rsp));

    CLOG(INFO, DICOM) << "Store SCU RQ: MsgID " << msgId << ", ("
                      << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ")";
    
    T_DIMSE_DetectedCancelParameters cancelParameters;
    memset(&cancelParameters, 0, sizeof(cancelParameters));

    std::unique_ptr<DcmDataset> stDetail;

    OFCondition cond;

    if (sourceSyntax == selectedSyntax)
    {
      // No transcoding is required
      DcmDataset *stDetailTmp = NULL;
      cond = DIMSE_storeUser(
        assoc, presId, &req, NULL /* imageFileName */, dicom->getDataset(),
        ProgressCallback, NULL /* callbackData */,
        (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_,
        &rsp, &stDetailTmp, &cancelParameters);
      stDetail.reset(stDetailTmp);
    }
    else
    {
      // Transcoding to the selected uncompressed transfer syntax
      IDicomTranscoder::DicomImage source, transcoded;
      source.AcquireParsed(dicom.release());

      std::set<DicomTransferSyntax> ts;
      ts.insert(selectedSyntax);
      
      if (context_.Transcode(transcoded, source, ts, true))
      {
        // Transcoding has succeeded
        DcmDataset *stDetailTmp = NULL;
        cond = DIMSE_storeUser(
          assoc, presId, &req, NULL /* imageFileName */,
          transcoded.GetParsed().getDataset(),
          ProgressCallback, NULL /* callbackData */,
          (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_,
          &rsp, &stDetailTmp, &cancelParameters);
        stDetail.reset(stDetailTmp);
      }
      else
      {
        // Cannot transcode
        failedCount_++;
        AddFailedUIDInstance(sopInstanceUid);
        throw OrthancException(ErrorCode_NotImplemented,
                               "C-GET SCP: Cannot transcode " + sopClassUid +
                               " from transfer syntax " + GetTransferSyntaxUid(sourceSyntax) +
                               " to " + GetTransferSyntaxUid(selectedSyntax));
      }      
    }

    bool isContinue;
    
    if (cond.good())
    {
      {
        OFString str;
        CLOG(TRACE, DICOM) << "Received Store Response following a C-GET:" << std::endl
                           << DIMSE_dumpMessage(str, rsp, DIMSE_INCOMING);
      }
      
      if (cancelParameters.cancelEncountered)
      {
        CLOG(INFO, DICOM) << "C-GET SCP: Received C-Cancel RQ";
        isContinue = false;
      }
      else if (rsp.DimseStatus == STATUS_Success)
      {
        // everything ok
        completedCount_++;
        isContinue = true;
      }
      else if ((rsp.DimseStatus & 0xf000) == 0xb000)
      {
        // a warning status message
        warningCount_++;
        CLOG(ERROR, DICOM) << "C-GET SCP: Store Warning: Response Status: "
                           << DU_cstoreStatusString(rsp.DimseStatus);
        isContinue = true;
      }
      else
      {
        failedCount_++;
        AddFailedUIDInstance(sopInstanceUid);
        // print a status message
        CLOG(ERROR, DICOM) << "C-GET SCP: Store Failed: Response Status: "
                           << DU_cstoreStatusString(rsp.DimseStatus);
        isContinue = true;
      }
    }
    else
    {
      failedCount_++;
      AddFailedUIDInstance(sopInstanceUid);
      OFString temp_str;
      CLOG(ERROR, DICOM) << "C-GET SCP: storeSCU: Store Request Failed: "
                         << DimseCondition::dump(temp_str, cond);
      isContinue = true;
    }
    
    if (stDetail.get() != NULL)
    {
      std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
      stDetail->print(s);
      CLOG(INFO, DICOM) << "  Status Detail: " << s.str();
    }
    
    return isContinue;
  }

  bool OrthancGetRequestHandler::LookupIdentifiers(std::list<std::string>& publicIds,
                                                   ResourceType level,
                                                   const DicomMap& input) const
  {
    DicomTag tag(0, 0);   // Dummy initialization

    switch (level)
    {
      case ResourceType_Patient:
        tag = DICOM_TAG_PATIENT_ID;
        break;

      case ResourceType_Study:
        tag = (input.HasTag(DICOM_TAG_ACCESSION_NUMBER) ?
               DICOM_TAG_ACCESSION_NUMBER : DICOM_TAG_STUDY_INSTANCE_UID);
        break;
        
      case ResourceType_Series:
        tag = DICOM_TAG_SERIES_INSTANCE_UID;
        break;
        
      case ResourceType_Instance:
        tag = DICOM_TAG_SOP_INSTANCE_UID;
        break;

      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }

    if (!input.HasTag(tag))
    {
      return false;
    }

    const DicomValue& value = input.GetValue(tag);
    if (value.IsNull() ||
        value.IsBinary())
    {
      return false;
    }
    else
    {
      std::vector<std::string> tokens;
      Toolbox::TokenizeString(tokens, value.GetContent(), '\\');

      for (size_t i = 0; i < tokens.size(); i++)
      {
        std::vector<std::string> tmp;
        context_.GetIndex().LookupIdentifierExact(tmp, level, tag, tokens[i]);

        if (tmp.empty())
        {
          CLOG(ERROR, DICOM) << "C-GET: Cannot locate resource \"" << tokens[i]
                             << "\" at the " << EnumerationToString(level) << " level";
          return false;
        }
        else
        {
          for (size_t j = 0; j < tmp.size(); j++)
          {
            publicIds.push_back(tmp[j]);
          }
        }
      }

      return true;      
    }
  }


  OrthancGetRequestHandler::OrthancGetRequestHandler(ServerContext& context) :
    context_(context),
    position_(0),
    completedCount_ (0),
    warningCount_(0),
    failedCount_(0),
    timeout_(0),
    allowTranscoding_(false)
  {
  }


  bool OrthancGetRequestHandler::Handle(const DicomMap& input,
                                        const std::string& originatorIp,
                                        const std::string& originatorAet,
                                        const std::string& calledAet,
                                        uint32_t timeout)
  {
    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_get_scp_duration_ms");

    CLOG(INFO, DICOM) << "C-GET-SCU request received from AET \"" << originatorAet << "\"";

    {
      DicomArray query(input);
      for (size_t i = 0; i < query.GetSize(); i++)
      {
        if (!query.GetElement(i).GetValue().IsNull())
        {
          CLOG(INFO, DICOM) << "  (" << query.GetElement(i).GetTag().Format()
                            << ")  " << FromDcmtkBridge::GetTagName(query.GetElement(i))
                            << " = " << context_.GetDeidentifiedContent(query.GetElement(i));
        }
      }
    }

    /**
     * Retrieve the query level.
     **/

    const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
    if (levelTmp == NULL ||
        levelTmp->IsNull() ||
        levelTmp->IsBinary())
    {
      throw OrthancException(ErrorCode_BadRequest,
                             "C-GET request without the tag 0008,0052 (QueryRetrieveLevel)");
    }

    ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());      


    /**
     * Lookup for the resource to be sent.
     **/

    std::list<std::string> publicIds;

    if (!LookupIdentifiers(publicIds, level, input))
    {
      CLOG(ERROR, DICOM) << "Cannot determine what resources are requested by C-GET";
      return false; 
    }

    localAet_ = context_.GetDefaultLocalApplicationEntityTitle();
    position_ = 0;
    originatorAet_ = originatorAet;
    
    {
      OrthancConfiguration::ReaderLock lock;

      RemoteModalityParameters remote;

      if (lock.GetConfiguration().LookupDicomModalityUsingAETitle(remote, originatorAet))
      {
        allowTranscoding_ = (context_.IsTranscodeDicomProtocol() &&
                             remote.IsTranscodingAllowed());
      }
      else if (lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowGet", false))
      {
        CLOG(INFO, DICOM) << "C-GET: Allowing SCU request from unknown modality with AET: " << originatorAet;
        allowTranscoding_ = context_.IsTranscodeDicomProtocol();
      }
      else
      {
        // This should never happen, given the test at bottom of
        // "OrthancApplicationEntityFilter::IsAllowedRequest()"
        throw OrthancException(ErrorCode_InexistentItem,
                               "C-GET: Rejecting SCU request from unknown modality with AET: " + originatorAet);
      }
    }

    for (std::list<std::string>::const_iterator
           resource = publicIds.begin(); resource != publicIds.end(); ++resource)
    {
      CLOG(INFO, DICOM) << "C-GET: Sending resource " << *resource
                        << " to modality \"" << originatorAet << "\"";
      
      std::list<std::string> tmp;
      context_.GetIndex().GetChildInstances(tmp, *resource);
      
      instances_.reserve(tmp.size());
      for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
      {
        instances_.push_back(*it);
      }
    }

    failedUIDs_.clear();

    completedCount_ = 0;
    failedCount_ = 0;
    warningCount_ = 0;
    timeout_ = timeout;

    return true;
  }
};