changeset 3736:0540b54324f1 storage-commitment

StorageCommitmentReports
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 10 Mar 2020 17:43:49 +0100
parents 77183afbf55e
children f29843323daf
files CMakeLists.txt Core/DicomNetworking/IStorageCommitmentRequestHandler.h Core/DicomNetworking/Internals/CommandDispatcher.cpp NEWS OrthancServer/ServerContext.cpp OrthancServer/ServerContext.h OrthancServer/StorageCommitmentReports.cpp OrthancServer/StorageCommitmentReports.h OrthancServer/main.cpp Resources/Configuration.json UnitTestsSources/MemoryCacheTests.cpp
diffstat 11 files changed, 501 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Tue Mar 10 13:59:14 2020 +0100
+++ b/CMakeLists.txt	Tue Mar 10 17:43:49 2020 +0100
@@ -106,6 +106,7 @@
   OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp
   OrthancServer/ServerToolbox.cpp
   OrthancServer/SliceOrdering.cpp
+  OrthancServer/StorageCommitmentReports.cpp
   )
 
 
--- a/Core/DicomNetworking/IStorageCommitmentRequestHandler.h	Tue Mar 10 13:59:14 2020 +0100
+++ b/Core/DicomNetworking/IStorageCommitmentRequestHandler.h	Tue Mar 10 17:43:49 2020 +0100
@@ -58,6 +58,7 @@
                               const std::vector<std::string>& successSopInstanceUids,
                               const std::vector<std::string>& failedSopClassUids,
                               const std::vector<std::string>& failedSopInstanceUids,
+                              const std::vector<StorageCommitmentFailureReason>& failureReasons,
                               const std::string& remoteIp,
                               const std::string& remoteAet,
                               const std::string& calledAet) = 0;
--- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Tue Mar 10 13:59:14 2020 +0100
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Tue Mar 10 17:43:49 2020 +0100
@@ -928,15 +928,22 @@
     }
 
 
-    static void ReadSopSequence(std::vector<std::string>& sopClassUids,
-                                std::vector<std::string>& sopInstanceUids,
-                                DcmDataset& dataset,
-                                const DcmTagKey& tag,
-                                bool mandatory)
+    static void ReadSopSequence(
+      std::vector<std::string>& sopClassUids,
+      std::vector<std::string>& sopInstanceUids,
+      std::vector<StorageCommitmentFailureReason>* failureReasons, // Can be NULL
+      DcmDataset& dataset,
+      const DcmTagKey& tag,
+      bool mandatory)
     {
       sopClassUids.clear();
       sopInstanceUids.clear();
 
+      if (failureReasons)
+      {
+        failureReasons->clear();
+      }
+
       DcmSequenceOfItems* sequence = NULL;
       if (!dataset.findAndGetSequence(tag, sequence).good() ||
           sequence == NULL)
@@ -957,6 +964,11 @@
       sopClassUids.reserve(sequence->card());
       sopInstanceUids.reserve(sequence->card());
 
+      if (failureReasons)
+      {
+        failureReasons->reserve(sequence->card());
+      }
+
       for (unsigned long i = 0; i < sequence->card(); i++)
       {
         const char* a = NULL;
@@ -968,11 +980,24 @@
         {
           throw OrthancException(ErrorCode_NetworkProtocol,
                                  "Missing Referenced SOP Class/Instance UID "
-                                 "in storage commitment request");
+                                 "in storage commitment dataset");
         }
 
         sopClassUids.push_back(a);
         sopInstanceUids.push_back(b);
+
+        if (failureReasons != NULL)
+        {
+          Uint16 reason;
+          if (!sequence->getItem(i)->findAndGetUint16(DCM_FailureReason, reason).good())
+          {
+            throw OrthancException(ErrorCode_NetworkProtocol,
+                                   "Missing Failure Reason (0008,1197) "
+                                   "in storage commitment dataset");
+          }
+
+          failureReasons->push_back(static_cast<StorageCommitmentFailureReason>(reason));
+        }
       }
     }
 
@@ -1034,7 +1059,7 @@
       std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
 
       std::vector<std::string> sopClassUid, sopInstanceUid;
-      ReadSopSequence(sopClassUid, sopInstanceUid,
+      ReadSopSequence(sopClassUid, sopInstanceUid, NULL,
                       *dataset, DCM_ReferencedSOPSequence, true /* mandatory */);
 
       LOG(INFO) << "Incoming storage commitment request, with transaction UID: " << transactionUid;
@@ -1157,15 +1182,16 @@
       std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
 
       std::vector<std::string> successSopClassUid, successSopInstanceUid;
-      ReadSopSequence(successSopClassUid, successSopInstanceUid,
+      ReadSopSequence(successSopClassUid, successSopInstanceUid, NULL,
                       *dataset, DCM_ReferencedSOPSequence,
                       (report.EventTypeID == 1) /* mandatory in the case of success */);
 
       std::vector<std::string> failedSopClassUid, failedSopInstanceUid;
+      std::vector<StorageCommitmentFailureReason> failureReasons;
 
       if (report.EventTypeID == 2 /* failures exist */)
       {
-        ReadSopSequence(failedSopClassUid, failedSopInstanceUid,
+        ReadSopSequence(failedSopClassUid, failedSopInstanceUid, &failureReasons,
                         *dataset, DCM_FailedSOPSequence, true);
       }
 
@@ -1200,7 +1226,7 @@
            ConstructStorageCommitmentRequestHandler());
 
         handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid,
-                              failedSopClassUid, failedSopInstanceUid,
+                              failedSopClassUid, failedSopInstanceUid, failureReasons,
                               remoteIp_, remoteAet_, calledAet_);
         
         dimseStatus = 0;  // Success
--- a/NEWS	Tue Mar 10 13:59:14 2020 +0100
+++ b/NEWS	Tue Mar 10 17:43:49 2020 +0100
@@ -40,6 +40,7 @@
 Maintenance
 -----------
 
+* New configuration options: "DefaultPrivateCreator" and "StorageCommitmentReportsSize"
 * Support of MPEG4 transfer syntaxes in C-Store SCP
 * C-FIND SCU at Instance level now sets the 0008,0052 tag to IMAGE per default (was INSTANCE).
   Therefore, the "ClearCanvas" and "Dcm4Chee" modality manufacturer have now been deprecated.
--- a/OrthancServer/ServerContext.cpp	Tue Mar 10 13:59:14 2020 +0100
+++ b/OrthancServer/ServerContext.cpp	Tue Mar 10 17:43:49 2020 +0100
@@ -49,6 +49,7 @@
 #include "Search/DatabaseLookup.h"
 #include "ServerJobs/OrthancJobUnserializer.h"
 #include "ServerToolbox.h"
+#include "StorageCommitmentReports.h"
 
 #include <EmbeddedResources.h>
 #include <dcmtk/dcmdata/dcfilefo.h>
@@ -259,6 +260,9 @@
       findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always"));
       limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0);
       limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0);
+
+      // New configuration option in Orthanc 1.6.0
+      storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100)));
     }
 
     jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
--- a/OrthancServer/ServerContext.h	Tue Mar 10 13:59:14 2020 +0100
+++ b/OrthancServer/ServerContext.h	Tue Mar 10 17:43:49 2020 +0100
@@ -54,6 +54,7 @@
   class SetOfInstancesJob;
   class SharedArchive;
   class SharedMessageQueue;
+  class StorageCommitmentReports;
   
   
   /**
@@ -221,6 +222,8 @@
     bool isHttpServerSecure_;
     bool isExecuteLuaEnabled_;
 
+    std::unique_ptr<StorageCommitmentReports>  storageCommitmentReports_;
+
   public:
     class DicomCacheLocker : public boost::noncopyable
     {
@@ -430,5 +433,10 @@
                             const std::vector<std::string>& sopInstanceUids,
                             const std::string& remoteAet,
                             const std::string& calledAet) ORTHANC_OVERRIDE;
+
+    StorageCommitmentReports& GetStorageCommitmentReports()
+    {
+      return *storageCommitmentReports_;
+    }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/StorageCommitmentReports.cpp	Tue Mar 10 17:43:49 2020 +0100
@@ -0,0 +1,185 @@
+/**
+ * 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 "StorageCommitmentReports.h"
+
+#include "../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  void StorageCommitmentReports::Report::MarkAsComplete()
+  {
+    if (isComplete_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      isComplete_ = true;
+    }
+  }
+
+  void StorageCommitmentReports::Report::AddSuccess(const std::string& sopClassUid,
+                                                    const std::string& sopInstanceUid)
+  {
+    if (isComplete_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      Success success;
+      success.sopClassUid_ = sopClassUid;
+      success.sopInstanceUid_ = sopInstanceUid;
+      successes_.push_back(success);
+    }
+  }
+
+  void StorageCommitmentReports::Report::AddFailure(const std::string& sopClassUid,
+                                                    const std::string& sopInstanceUid,
+                                                    StorageCommitmentFailureReason reason)
+  {
+    if (isComplete_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      Failure failure;
+      failure.sopClassUid_ = sopClassUid;
+      failure.sopInstanceUid_ = sopInstanceUid;
+      failure.reason_ = reason;
+      failures_.push_back(failure);
+    }
+  }
+
+  
+  StorageCommitmentReports::Report::Status StorageCommitmentReports::Report::GetStatus() const
+  {
+    if (!isComplete_)
+    {
+      return Status_Pending;
+    }
+    else if (failures_.empty())
+    {
+      return Status_Success;
+    }
+    else
+    {
+      return Status_Failure;
+    }
+  }
+
+
+  StorageCommitmentReports::~StorageCommitmentReports()
+  {
+    while (!content_.IsEmpty())
+    {
+      Report* report = NULL;
+      content_.RemoveOldest(report);
+
+      assert(report != NULL);
+      delete report;
+    }
+  }
+
+  
+  void StorageCommitmentReports::Store(const std::string& transactionUid,
+                                       Report* report)
+  {
+    std::unique_ptr<Report> protection(report);
+    
+    boost::mutex::scoped_lock lock(mutex_);
+
+    {
+      Report* previous = NULL;
+      if (content_.Contains(transactionUid, previous))
+      {
+        assert(previous != NULL);
+        delete previous;
+
+        content_.Invalidate(transactionUid);
+      }
+    }
+
+    assert(maxSize_ == 0 ||
+           content_.GetSize() <= maxSize_);
+
+    if (maxSize_ != 0 &&
+        content_.GetSize() == maxSize_)
+    {
+      assert(!content_.IsEmpty());
+      
+      Report* oldest = NULL;
+      content_.RemoveOldest(oldest);
+
+      assert(oldest != NULL);
+      delete oldest;
+    }
+
+    assert(maxSize_ == 0 ||
+           content_.GetSize() < maxSize_);
+
+    content_.Add(transactionUid, protection.release());
+  }
+
+
+  StorageCommitmentReports::Accessor::Accessor(StorageCommitmentReports& that,
+                                               const std::string& transactionUid) :
+    lock_(that.mutex_),
+    transactionUid_(transactionUid)
+  {
+    if (that.content_.Contains(transactionUid, report_))
+    {
+      that.content_.MakeMostRecent(transactionUid);
+    }
+    else
+    {
+      report_ = NULL;
+    }
+  }
+
+  const StorageCommitmentReports::Report&
+  StorageCommitmentReports::Accessor::GetReport() const
+  {
+    if (report_ == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *report_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/StorageCommitmentReports.h	Tue Mar 10 17:43:49 2020 +0100
@@ -0,0 +1,143 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "../Core/Cache/LeastRecentlyUsedIndex.h"
+
+namespace Orthanc
+{
+  class StorageCommitmentReports
+  {
+  public:
+    class Report : public boost::noncopyable
+    {
+    public:
+      enum Status
+      {
+        Status_Success,
+        Status_Failure,
+        Status_Pending
+      };
+      
+    private:
+      struct Success
+      {
+        std::string  sopClassUid_;
+        std::string  sopInstanceUid_;
+      };
+      
+      struct Failure
+      {
+        std::string  sopClassUid_;
+        std::string  sopInstanceUid_;
+        StorageCommitmentFailureReason  reason_;
+      };
+      
+      bool                isComplete_;
+      std::list<Success>  successes_;
+      std::list<Failure>  failures_;
+      std::string         remoteAet_;
+
+    public:
+      Report(const std::string& remoteAet) :
+        isComplete_(false),
+        remoteAet_(remoteAet)
+      {
+      }
+
+      const std::string& GetRemoteAet() const
+      {
+        return remoteAet_;
+      }
+
+      void MarkAsComplete();
+
+      void AddSuccess(const std::string& sopClassUid,
+                      const std::string& sopInstanceUid);
+
+      void AddFailure(const std::string& sopClassUid,
+                      const std::string& sopInstanceUid,
+                      StorageCommitmentFailureReason reason);
+
+      Status GetStatus() const;
+    };
+
+  private:
+    typedef LeastRecentlyUsedIndex<std::string, Report*>  Content;
+    
+    boost::mutex   mutex_;
+    Content        content_;
+    size_t         maxSize_;
+
+  public:
+    StorageCommitmentReports(size_t maxSize) :
+      maxSize_(maxSize)
+    {
+    }
+
+    ~StorageCommitmentReports();
+
+    size_t GetMaxSize() const
+    {
+      return maxSize_;
+    }
+
+    void Store(const std::string& transactionUid,
+               Report* report); // Takes ownership
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock  lock_;
+      std::string                transactionUid_;
+      Report                    *report_;
+
+    public:
+      Accessor(StorageCommitmentReports& that,
+               const std::string& transactionUid);
+
+      const std::string& GetTransactionUid() const
+      {
+        return transactionUid_;
+      }
+
+      bool IsValid() const
+      {
+        return report_ != NULL;
+      }
+
+      const Report& GetReport() const;
+    };
+  };
+}
--- a/OrthancServer/main.cpp	Tue Mar 10 13:59:14 2020 +0100
+++ b/OrthancServer/main.cpp	Tue Mar 10 17:43:49 2020 +0100
@@ -52,6 +52,7 @@
 #include "ServerContext.h"
 #include "ServerJobs/StorageCommitmentScpJob.h"
 #include "ServerToolbox.h"
+#include "StorageCommitmentReports.h"
 
 using namespace Orthanc;
 
@@ -133,12 +134,34 @@
                             const std::vector<std::string>& successSopInstanceUids,
                             const std::vector<std::string>& failedSopClassUids,
                             const std::vector<std::string>& failedSopInstanceUids,
+                            const std::vector<StorageCommitmentFailureReason>& failureReasons,
                             const std::string& remoteIp,
                             const std::string& remoteAet,
                             const std::string& calledAet)
   {
-    // TODO
-    printf("HANDLE REPORT\n");
+    if (successSopClassUids.size() != successSopInstanceUids.size() ||
+        failedSopClassUids.size() != failedSopInstanceUids.size() ||
+        failedSopClassUids.size() != failureReasons.size())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    std::unique_ptr<StorageCommitmentReports::Report> report(
+      new StorageCommitmentReports::Report(remoteAet));
+
+    for (size_t i = 0; i < successSopClassUids.size(); i++)
+    {
+      report->AddSuccess(successSopClassUids[i], successSopInstanceUids[i]);
+    }
+
+    for (size_t i = 0; i < failedSopClassUids.size(); i++)
+    {
+      report->AddFailure(failedSopClassUids[i], failedSopInstanceUids[i], failureReasons[i]);
+    }
+
+    report->MarkAsComplete();
+
+    context_.GetStorageCommitmentReports().Store(transactionUid, report.release());
   }
 };
 
--- a/Resources/Configuration.json	Tue Mar 10 13:59:14 2020 +0100
+++ b/Resources/Configuration.json	Tue Mar 10 17:43:49 2020 +0100
@@ -528,5 +528,9 @@
   // Set the default private creator that is used by Orthanc when it
   // looks for a private tag in its dictionary (cf. "Dictionary"
   // option), or when it creates/modifies a DICOM file (new in Orthanc 1.6.0).
-  "DefaultPrivateCreator" : ""
+  "DefaultPrivateCreator" : "",
+
+  // Maximum number of storage commitment reports (i.e. received from
+  // remote modalities) to be kept in memory (new in Orthanc 1.6.0).
+  "StorageCommitmentReportsSize" : 100
 }
--- a/UnitTestsSources/MemoryCacheTests.cpp	Tue Mar 10 13:59:14 2020 +0100
+++ b/UnitTestsSources/MemoryCacheTests.cpp	Tue Mar 10 17:43:49 2020 +0100
@@ -44,6 +44,7 @@
 #include "../Core/Cache/SharedArchive.h"
 #include "../Core/IDynamicObject.h"
 #include "../Core/Logging.h"
+#include "../OrthancServer/StorageCommitmentReports.h"
 
 
 TEST(LRU, Basic)
@@ -366,3 +367,94 @@
   ASSERT_FALSE(c.Fetch(v, "hello"));
   ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
 }
+
+
+TEST(StorageCommitmentReports, Basic)
+{
+  Orthanc::StorageCommitmentReports reports(2);
+  ASSERT_EQ(2u, reports.GetMaxSize());
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "nope");
+    ASSERT_EQ("nope", accessor.GetTransactionUid());
+    ASSERT_FALSE(accessor.IsValid());
+    ASSERT_THROW(accessor.GetReport(), Orthanc::OrthancException);
+  }
+
+  reports.Store("a", new Orthanc::StorageCommitmentReports::Report("aet_a"));
+  reports.Store("b", new Orthanc::StorageCommitmentReports::Report("aet_b"));
+  reports.Store("c", new Orthanc::StorageCommitmentReports::Report("aet_c"));
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
+    ASSERT_FALSE(accessor.IsValid());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
+    ASSERT_TRUE(accessor.IsValid());
+    ASSERT_EQ("aet_b", accessor.GetReport().GetRemoteAet());
+    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Pending,
+              accessor.GetReport().GetStatus());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
+    ASSERT_EQ("aet_c", accessor.GetReport().GetRemoteAet());
+    ASSERT_TRUE(accessor.IsValid());
+  }
+
+  {
+    std::unique_ptr<Orthanc::StorageCommitmentReports::Report> report
+      (new Orthanc::StorageCommitmentReports::Report("aet"));
+    report->AddSuccess("class1", "instance1");
+    report->AddFailure("class2", "instance2",
+                       Orthanc::StorageCommitmentFailureReason_ReferencedSOPClassNotSupported);
+    report->MarkAsComplete();
+    reports.Store("a", report.release());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
+    ASSERT_TRUE(accessor.IsValid());
+    ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet());
+    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Failure,
+              accessor.GetReport().GetStatus());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
+    ASSERT_FALSE(accessor.IsValid());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
+    ASSERT_TRUE(accessor.IsValid());
+  }
+
+  {
+    std::unique_ptr<Orthanc::StorageCommitmentReports::Report> report
+      (new Orthanc::StorageCommitmentReports::Report("aet"));
+    report->AddSuccess("class1", "instance1");
+    report->MarkAsComplete();
+    reports.Store("a", report.release());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
+    ASSERT_TRUE(accessor.IsValid());
+    ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet());
+    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Success,
+              accessor.GetReport().GetStatus());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
+    ASSERT_FALSE(accessor.IsValid());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
+    ASSERT_TRUE(accessor.IsValid());
+  }
+}