# HG changeset patch # User Sebastien Jodogne # Date 1583842922 -3600 # Node ID 4fc24b69446ae858097039dcd1fc178444c291b7 # Parent e7ff4f9b34bdbad86c3732923255d08a1883f0e4 triggering storage commitment scu from REST API diff -r e7ff4f9b34bd -r 4fc24b69446a Core/DicomNetworking/DicomUserConnection.cpp --- a/Core/DicomNetworking/DicomUserConnection.cpp Mon Mar 09 17:19:45 2020 +0100 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Tue Mar 10 13:22:02 2020 +0100 @@ -158,7 +158,9 @@ void CheckIsOpen() const; - void Store(DcmInputStream& is, + void Store(std::string& sopClassUidOut /* out */, + std::string& sopInstanceUidOut /* out */, + DcmInputStream& is, DicomUserConnection& connection, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); @@ -351,7 +353,9 @@ } - void DicomUserConnection::PImpl::Store(DcmInputStream& is, + void DicomUserConnection::PImpl::Store(std::string& sopClassUidOut, + std::string& sopInstanceUidOut, + DcmInputStream& is, DicomUserConnection& connection, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) @@ -434,6 +438,9 @@ connection.remoteAet_); } + sopClassUidOut.assign(sopClass); + sopInstanceUidOut.assign(sopInstance); + // Figure out which of the accepted presentation contexts should be used int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass); if (presID == 0) @@ -1175,7 +1182,9 @@ return pimpl_->IsOpen(); } - void DicomUserConnection::Store(const char* buffer, + void DicomUserConnection::Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const char* buffer, size_t size, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) @@ -1186,26 +1195,31 @@ is.setBuffer(buffer, size); is.setEos(); - pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); + pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID); } - void DicomUserConnection::Store(const std::string& buffer, + void DicomUserConnection::Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& buffer, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) { if (buffer.size() > 0) - Store(&buffer[0], buffer.size(), moveOriginatorAET, moveOriginatorID); + Store(sopClassUid, sopInstanceUid, &buffer[0], buffer.size(), + moveOriginatorAET, moveOriginatorID); else - Store(NULL, 0, moveOriginatorAET, moveOriginatorID); + Store(sopClassUid, sopInstanceUid, NULL, 0, moveOriginatorAET, moveOriginatorID); } - void DicomUserConnection::StoreFile(const std::string& path, + void DicomUserConnection::StoreFile(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& path, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) { // Prepare an input stream for the file DcmInputFileStream is(path.c_str()); - pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); + pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID); } bool DicomUserConnection::Echo() diff -r e7ff4f9b34bd -r 4fc24b69446a Core/DicomNetworking/DicomUserConnection.h --- a/Core/DicomNetworking/DicomUserConnection.h Mon Mar 09 17:19:45 2020 +0100 +++ b/Core/DicomNetworking/DicomUserConnection.h Tue Mar 10 13:22:02 2020 +0100 @@ -158,33 +158,45 @@ bool Echo(); - void Store(const char* buffer, + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const char* buffer, size_t size, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); - void Store(const char* buffer, + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const char* buffer, size_t size) { - Store(buffer, size, "", 0); // Not a C-Move + Store(sopClassUid, sopInstanceUid, buffer, size, "", 0); // Not a C-Move } - void Store(const std::string& buffer, + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& buffer, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); - void Store(const std::string& buffer) + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& buffer) { - Store(buffer, "", 0); // Not a C-Move + Store(sopClassUid, sopInstanceUid, buffer, "", 0); // Not a C-Move } - void StoreFile(const std::string& path, + void StoreFile(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& path, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); - void StoreFile(const std::string& path) + void StoreFile(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& path) { - StoreFile(path, "", 0); // Not a C-Move + StoreFile(sopClassUid, sopInstanceUid, path, "", 0); // Not a C-Move } void Find(DicomFindAnswers& result, diff -r e7ff4f9b34bd -r 4fc24b69446a NEWS --- a/NEWS Mon Mar 09 17:19:45 2020 +0100 +++ b/NEWS Tue Mar 10 13:22:02 2020 +0100 @@ -10,15 +10,17 @@ -------- * API version has been upgraded to 5 -* added "/peers/{id}/system" route to test the connectivity with a remote peer (and eventually +* Added "/peers/{id}/system" route to test the connectivity with a remote peer (and eventually retrieve its version number) * "/changes": Allow the "limit" argument to be greater than 100 * "/instances/{id}/preview": Now takes the windowing into account * "/tools/log-level": Possibility to access and change the log level without restarting Orthanc -* added "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered" routes +* Added "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered" routes to render frames, taking windowing and resizing into account * "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files * ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags +* Added "/modalities/{...}/storage-commitment" route +* "/modalities/{...}/store" now accepts the Boolean argument "StorageCommitment" Plugins ------- diff -r e7ff4f9b34bd -r 4fc24b69446a OrthancServer/OrthancMoveRequestHandler.cpp --- a/OrthancServer/OrthancMoveRequestHandler.cpp Mon Mar 09 17:19:45 2020 +0100 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Tue Mar 10 13:22:02 2020 +0100 @@ -116,7 +116,8 @@ connection_.reset(new DicomUserConnection(localAet_, remote_)); } - connection_->Store(dicom, originatorAet_, originatorId_); + std::string sopClassUid, sopInstanceUid; // Unused + connection_->Store(sopClassUid, sopInstanceUid, dicom, originatorAet_, originatorId_); return Status_Success; } diff -r e7ff4f9b34bd -r 4fc24b69446a OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Mon Mar 09 17:19:45 2020 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Tue Mar 10 13:22:02 2020 +0100 @@ -963,6 +963,12 @@ job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID); } + // New in Orthanc 1.6.0 + if (Toolbox::GetJsonBooleanField(request, "StorageCommitment", false)) + { + job->EnableStorageCommitment(true); + } + OrthancRestApi::GetApi(call).SubmitCommandsJob (call, job.release(), true /* synchronous by default */, request); } @@ -1299,36 +1305,85 @@ } - static void TestStorageCommitment(RestApiPostCall& call) + static void StorageCommitment(RestApiPostCall& call) { ServerContext& context = OrthancRestApi::GetContext(call); Json::Value json; - if (call.ParseJsonRequest(json)) + if (call.ParseJsonRequest(json) || + json.type() != Json::arrayValue) { - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - const RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + std::vector sopClassUids, sopInstanceUids; + sopClassUids.resize(json.size()); + sopInstanceUids.resize(json.size()); + + for (Json::Value::ArrayIndex i = 0; i < json.size(); i++) + { + if (json[i].type() == Json::arrayValue) + { + if (json[i].size() != 2 || + json[i][0].type() != Json::stringValue || + json[i][1].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "An instance entry must provide an array with 2 strings: " + "SOP Class UID and SOP Instance UID"); + } + else + { + sopClassUids[i] = json[i][0].asString(); + sopInstanceUids[i] = json[i][1].asString(); + } + } + else if (json[i].type() == Json::objectValue) + { + static const char* const SOP_CLASS_UID = "SOPClassUID"; + static const char* const SOP_INSTANCE_UID = "SOPInstanceUID"; + + if (!json[i].isMember(SOP_CLASS_UID) || + !json[i].isMember(SOP_INSTANCE_UID) || + json[i][SOP_CLASS_UID].type() != Json::stringValue || + json[i][SOP_INSTANCE_UID].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat, + "An instance entry must provide an object with 2 string fiels: " + "\"" + std::string(SOP_CLASS_UID) + "\" and \"" + + std::string(SOP_INSTANCE_UID)); + } + else + { + sopClassUids[i] = json[i][SOP_CLASS_UID].asString(); + sopInstanceUids[i] = json[i][SOP_INSTANCE_UID].asString(); + } + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "JSON array or object is expected to specify one " + "instance to be queried, found: " + json[i].toStyledString()); + } + } + + const std::string transaction = Toolbox::GenerateDicomPrivateUniqueIdentifier(); { - DicomUserConnection scu(localAet, remote); + const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); + const RemoteModalityParameters remote = + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - std::vector sopClassUids, sopInstanceUids; - sopClassUids.push_back("a"); - sopInstanceUids.push_back("b"); - sopClassUids.push_back("1.2.840.10008.5.1.4.1.1.6.1"); - sopInstanceUids.push_back("1.2.840.113543.6.6.4.7.64234348190163144631511103849051737563212"); - - std::string t = Toolbox::GenerateDicomPrivateUniqueIdentifier(); - scu.RequestStorageCommitment(t, sopClassUids, sopInstanceUids); + DicomUserConnection scu(localAet, remote); + scu.RequestStorageCommitment(transaction, sopClassUids, sopInstanceUids); } - Json::Value result; + Json::Value result = Json::objectValue; + result["ID"] = transaction; + result["Path"] = "/storage-commitment/" + transaction; call.GetOutput().AnswerJson(result); } else { - throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object"); + throw OrthancException(ErrorCode_BadFileFormat, + "Must provide a JSON array with a list of instances"); } } @@ -1377,6 +1432,6 @@ Register("/modalities/{id}/find-worklist", DicomFindWorklist); - Register("/modalities/{id}/storage-commitment", TestStorageCommitment); + Register("/modalities/{id}/storage-commitment", StorageCommitment); } } diff -r e7ff4f9b34bd -r 4fc24b69446a OrthancServer/ServerJobs/DicomModalityStoreJob.cpp --- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Mon Mar 09 17:19:45 2020 +0100 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Tue Mar 10 13:22:02 2020 +0100 @@ -72,14 +72,39 @@ LOG(WARNING) << "An instance was removed after the job was issued: " << instance; return false; } + + std::string sopClassUid, sopInstanceUid; if (HasMoveOriginator()) { - connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_); + connection_->Store(sopClassUid, sopInstanceUid, dicom, moveOriginatorAet_, moveOriginatorId_); } else { - connection_->Store(dicom); + connection_->Store(sopClassUid, sopInstanceUid, dicom); + } + + if (storageCommitment_) + { + sopClassUids_.push_back(sopClassUid); + sopInstanceUids_.push_back(sopInstanceUid); + + if (sopClassUids_.size() != sopInstanceUids_.size() || + sopClassUids_.size() > GetInstancesCount()) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (sopClassUids_.size() == GetInstancesCount()) + { + LOG(INFO) << "Sending storage commitment request to modality: " + << remote_.GetApplicationEntityTitle(); + + assert(IsStarted()); + OpenConnection(); + + connection_->RequestStorageCommitment(transactionUid_, sopClassUids_, sopInstanceUids_); + } } //boost::this_thread::sleep(boost::posix_time::milliseconds(500)); @@ -97,7 +122,8 @@ DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) : context_(context), localAet_("ORTHANC"), - moveOriginatorId_(0) // By default, not a C-MOVE + moveOriginatorId_(0), // By default, not a C-MOVE + storageCommitment_(false) // By default, no storage commitment { } @@ -179,6 +205,44 @@ } + void DicomModalityStoreJob::ResetStorageCommitment() + { + if (storageCommitment_) + { + transactionUid_ = Toolbox::GenerateDicomPrivateUniqueIdentifier(); + sopClassUids_.reserve(GetInstancesCount()); + sopInstanceUids_.reserve(GetInstancesCount()); + } + } + + + void DicomModalityStoreJob::Start() + { + SetOfInstancesJob::Start(); + ResetStorageCommitment(); + } + + + void DicomModalityStoreJob::Reset() + { + SetOfInstancesJob::Reset(); + + /** + * "After the N-EVENT-REPORT has been sent, the Transaction UID is + * no longer active and shall not be reused for other + * transactions." => Need to reset the transaction UID here + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html + **/ + ResetStorageCommitment(); + } + + + void DicomModalityStoreJob::EnableStorageCommitment(bool enabled) + { + storageCommitment_ = enabled; + } + + void DicomModalityStoreJob::GetPublicContent(Json::Value& value) { SetOfInstancesJob::GetPublicContent(value); @@ -191,6 +255,11 @@ value["MoveOriginatorAET"] = GetMoveOriginatorAet(); value["MoveOriginatorID"] = GetMoveOriginatorId(); } + + if (storageCommitment_) + { + value["StorageCommitmentTransactionUID"] = transactionUid_; + } } @@ -198,6 +267,7 @@ static const char* REMOTE = "Remote"; static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet"; static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId"; + static const char* STORAGE_COMMITMENT = "StorageCommitment"; DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context, @@ -210,6 +280,7 @@ moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET); moveOriginatorId_ = static_cast (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID)); + EnableStorageCommitment(SerializationToolbox::ReadBoolean(serialized, STORAGE_COMMITMENT)); } @@ -225,6 +296,7 @@ remote_.Serialize(target[REMOTE], true /* force advanced format */); target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_; target[MOVE_ORIGINATOR_ID] = moveOriginatorId_; + target[STORAGE_COMMITMENT] = storageCommitment_; return true; } } diff -r e7ff4f9b34bd -r 4fc24b69446a OrthancServer/ServerJobs/DicomModalityStoreJob.h --- a/OrthancServer/ServerJobs/DicomModalityStoreJob.h Mon Mar 09 17:19:45 2020 +0100 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h Tue Mar 10 13:22:02 2020 +0100 @@ -50,9 +50,17 @@ std::string moveOriginatorAet_; uint16_t moveOriginatorId_; std::unique_ptr connection_; + bool storageCommitment_; + + // For storage commitment + std::string transactionUid_; + std::vector sopInstanceUids_; + std::vector sopClassUids_; void OpenConnection(); + void ResetStorageCommitment(); + protected: virtual bool HandleInstance(const std::string& instance); @@ -90,7 +98,7 @@ void SetMoveOriginator(const std::string& aet, int id); - virtual void Stop(JobStopReason reason); + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; virtual void GetJobType(std::string& target) { @@ -100,5 +108,11 @@ virtual void GetPublicContent(Json::Value& value); virtual bool Serialize(Json::Value& target); + + virtual void Start() ORTHANC_OVERRIDE; + + virtual void Reset() ORTHANC_OVERRIDE; + + void EnableStorageCommitment(bool enabled); }; } diff -r e7ff4f9b34bd -r 4fc24b69446a OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp --- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp Mon Mar 09 17:19:45 2020 +0100 +++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp Tue Mar 10 13:22:02 2020 +0100 @@ -70,7 +70,9 @@ { std::string dicom; instance.ReadDicom(dicom); - resource->GetConnection().Store(dicom); + + std::string sopClassUid, sopInstanceUid; // Unused + resource->GetConnection().Store(sopClassUid, sopInstanceUid, dicom); } catch (OrthancException& e) { diff -r e7ff4f9b34bd -r 4fc24b69446a OrthancServer/main.cpp --- a/OrthancServer/main.cpp Mon Mar 09 17:19:45 2020 +0100 +++ b/OrthancServer/main.cpp Tue Mar 10 13:22:02 2020 +0100 @@ -137,14 +137,8 @@ const std::string& remoteAet, const std::string& calledAet) { + // TODO printf("HANDLE REPORT\n"); - - /** - * "After the N-EVENT-REPORT has been sent, the Transaction UID is - * no longer active and shall not be reused for other - * transactions." - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html - **/ } };