diff OrthancServer/Sources/OrthancGetRequestHandler.cpp @ 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents OrthancServer/OrthancGetRequestHandler.cpp@7f8b30416d50
children 05b8fd21089c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,583 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., 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.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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 "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/Logging.h"
+#include "../Core/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
+{
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+    
+    static void GetSubOpProgressCallback(
+      void * /* callbackData == pointer to the "OrthancGetRequestHandler" object */,
+      T_DIMSE_StoreProgress *progress,
+      T_DIMSE_C_StoreRQ * /*req*/)
+    {
+      // SBL - no logging to be done here.
+    }
+  }
+
+  OrthancGetRequestHandler::Status
+  OrthancGetRequestHandler::DoNext(T_ASC_Association* assoc)
+  {
+    if (position_ >= instances_.size())
+    {
+      return Status_Failure;
+    }
+    
+    const std::string& id = instances_[position_++];
+
+    std::string dicom;
+    context_.ReadDicom(dicom, id);
+    
+    if (dicom.size() <= 0)
+    {
+      return Status_Failure;
+    }
+
+    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());
+    
+    OFCondition cond = PerformGetSubOp(assoc, sopClassUid, sopInstanceUid, parsed.release());
+    
+    if (getCancelled_)
+    {
+      LOG(INFO) << "C-GET SCP: Received C-Cancel RQ";
+    }
+    
+    if (cond.bad() || getCancelled_)
+    {
+      return Status_Failure;
+    }
+    
+    return Status_Success;
+  }
+
+  
+  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 &&
+            LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
+        {
+          VLOG(0) << "C-GET SCP accepted: SOP class " << sopClassUid
+                  << " with transfer syntax " << GetTransferSyntaxUid(transferSyntax);
+          if (std::string(pc->abstractSyntax) == sopClassUid)
+          {
+            accepted[transferSyntax] = pc->presentationContextID;
+          }
+        }
+        else
+        {
+          LOG(WARNING) << "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.
+     **/
+    
+    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
+    return false;
+  }                                                           
+
+
+  OFCondition 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))
+    {
+      nFailed_++;
+      AddFailedUIDInstance(sopInstanceUid);
+      LOG(ERROR) << "C-GET SCP: Unknown transfer syntax: ("
+                 << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
+      return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
+    }
+
+    bool allowTranscoding = (context_.IsTranscodeDicomProtocol() &&
+                             remote_.IsTranscodingAllowed());
+    
+    T_ASC_PresentationContextID presId = 0;  // Unnecessary initialization, makes code clearer
+    DicomTransferSyntax selectedSyntax;
+    if (!SelectPresentationContext(presId, selectedSyntax, assoc, sopClassUid,
+                                   sourceSyntax, allowTranscoding) ||
+        presId == 0)
+    {
+      nFailed_++;
+      AddFailedUIDInstance(sopInstanceUid);
+      LOG(ERROR) << "C-GET SCP: storeSCU: No presentation context for: ("
+                 << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
+      return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
+    }
+    else
+    {
+      LOG(INFO) << "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
+        nFailed_++;
+        AddFailedUIDInstance(sopInstanceUid);
+        LOG(ERROR) << "C-GET SCP: storeSCU: [No presentation context with requestor SCP role for: ("
+                   << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
+        return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
+      }
+    }
+
+    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));
+
+    LOG(INFO) << "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(),
+        GetSubOpProgressCallback, this /* 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(),
+          GetSubOpProgressCallback, this /* callbackData */,
+          (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_,
+          &rsp, &stDetailTmp, &cancelParameters);
+        stDetail.reset(stDetailTmp);
+      }
+      else
+      {
+        // Cannot transcode
+        nFailed_++;
+        AddFailedUIDInstance(sopInstanceUid);
+        LOG(ERROR) << "C-GET SCP: Cannot transcode " << sopClassUid
+                   << " from transfer syntax " << GetTransferSyntaxUid(sourceSyntax)
+                   << " to " << GetTransferSyntaxUid(selectedSyntax);
+        return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
+      }      
+    }
+    
+    if (cond.good())
+    {
+      if (cancelParameters.cancelEncountered)
+      {
+        if (origPresId_ == cancelParameters.presId &&
+            origMsgId_ == cancelParameters.req.MessageIDBeingRespondedTo)
+        {
+          getCancelled_ = OFTrue;
+        }
+        else
+        {
+          LOG(ERROR) << "C-GET SCP: Unexpected C-Cancel-RQ encountered: pid=" << (int)cancelParameters.presId
+                     << ", mid=" << (int)cancelParameters.req.MessageIDBeingRespondedTo;
+        }
+      }
+      
+      if (rsp.DimseStatus == STATUS_Success)
+      {
+        // everything ok
+        nCompleted_++;
+      }
+      else if ((rsp.DimseStatus & 0xf000) == 0xb000)
+      {
+        // a warning status message
+        warningCount_++;
+        LOG(ERROR) << "C-GET SCP: Store Warning: Response Status: "
+                   << DU_cstoreStatusString(rsp.DimseStatus);
+      }
+      else
+      {
+        nFailed_++;
+        AddFailedUIDInstance(sopInstanceUid);
+        // print a status message
+        LOG(ERROR) << "C-GET SCP: Store Failed: Response Status: "
+                   << DU_cstoreStatusString(rsp.DimseStatus);
+      }
+    }
+    else
+    {
+      nFailed_++;
+      AddFailedUIDInstance(sopInstanceUid);
+      OFString temp_str;
+      LOG(ERROR) << "C-GET SCP: storeSCU: Store Request Failed: " << DimseCondition::dump(temp_str, cond);
+    }
+    
+    if (stDetail.get() != NULL)
+    {
+      // It is impossible to directly use the "<<" stream construct
+      // with "DcmObject::PrintHelper" using MSVC2008
+      std::stringstream s;
+      DcmObject::PrintHelper obj(*stDetail);
+      obj.dcmobj_.print(s);
+
+      LOG(INFO) << "  Status Detail: " << s.str();
+    }
+    
+    return cond;
+  }
+
+  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())
+        {
+          LOG(ERROR) << "C-GET: Cannot locate resource \"" << tokens[i]
+                     << "\" at the " << EnumerationToString(level) << " level";
+          return false;
+        }
+        else
+        {
+          for (size_t i = 0; i < tmp.size(); i++)
+          {
+            publicIds.push_back(tmp[i]);
+          }
+        }
+      }
+
+      return true;      
+    }
+  }
+
+
+    OrthancGetRequestHandler::OrthancGetRequestHandler(ServerContext& context) :
+      context_(context)
+    {
+      position_ = 0;
+      nRemaining_ = 0;
+      nCompleted_  = 0;
+      warningCount_ = 0;
+      nFailed_ = 0;
+      timeout_ = 0;
+    }
+
+
+  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");
+
+    LOG(WARNING) << "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())
+        {
+          LOG(INFO) << "  " << query.GetElement(i).GetTag()
+                    << "  " << FromDcmtkBridge::GetTagName(query.GetElement(i))
+                    << " = " << query.GetElement(i).GetValue().GetContent();
+        }
+      }
+    }
+
+    /**
+     * Retrieve the query level.
+     **/
+
+    const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+
+    assert(levelTmp != NULL);
+    ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());      
+
+
+    /**
+     * Lookup for the resource to be sent.
+     **/
+
+    std::list<std::string> publicIds;
+
+    if (!LookupIdentifiers(publicIds, level, input))
+    {
+      LOG(ERROR) << "Cannot determine what resources are requested by C-GET";
+      return false; 
+    }
+
+    localAet_ = context_.GetDefaultLocalApplicationEntityTitle();
+    position_ = 0;
+    originatorAet_ = originatorAet;
+    
+    {
+      OrthancConfiguration::ReaderLock lock;
+      remote_ = lock.GetConfiguration().GetModalityUsingAet(originatorAet);
+    }
+
+    for (std::list<std::string>::const_iterator
+           resource = publicIds.begin(); resource != publicIds.end(); ++resource)
+    {
+      LOG(INFO) << "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();
+    getCancelled_ = OFFalse;
+
+    nRemaining_ = GetSubOperationCount();
+    nCompleted_ = 0;
+    nFailed_ = 0;
+    warningCount_ = 0;
+    timeout_ = timeout;
+
+    return true;
+  }
+};