# HG changeset patch # User Sebastien Jodogne # Date 1538146123 -7200 # Node ID 99863d6245b2adf316e38eca8c3276b5492be9cd # Parent 4ee3a759afeadcf04775a3cc3d6c9c2ea9dabb85 New URI: "/studies/.../split" to split a study diff -r 4ee3a759afea -r 99863d6245b2 CMakeLists.txt --- a/CMakeLists.txt Thu Sep 27 13:22:57 2018 +0200 +++ b/CMakeLists.txt Fri Sep 28 16:48:43 2018 +0200 @@ -93,6 +93,7 @@ OrthancServer/ServerJobs/OrthancJobUnserializer.cpp OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp OrthancServer/ServerJobs/ResourceModificationJob.cpp + OrthancServer/ServerJobs/SplitStudyJob.cpp OrthancServer/ServerToolbox.cpp OrthancServer/SliceOrdering.cpp ) diff -r 4ee3a759afea -r 99863d6245b2 Core/DicomParsing/DicomModification.cpp --- a/Core/DicomParsing/DicomModification.cpp Thu Sep 27 13:22:57 2018 +0200 +++ b/Core/DicomParsing/DicomModification.cpp Fri Sep 28 16:48:43 2018 +0200 @@ -1071,6 +1071,11 @@ for (Json::Value::ArrayIndex i = 0; i < query.size(); i++) { + if (query[i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest); + } + std::string name = query[i].asString(); DicomTag tag = FromDcmtkBridge::ParseTag(name); diff -r 4ee3a759afea -r 99863d6245b2 NEWS --- a/NEWS Thu Sep 27 13:22:57 2018 +0200 +++ b/NEWS Fri Sep 28 16:48:43 2018 +0200 @@ -1,6 +1,12 @@ Pending changes in the mainline =============================== + +REST API +-------- + +* New URI: "/studies/.../split" to split a study + Maintenance ----------- diff -r 4ee3a759afea -r 99863d6245b2 OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu Sep 27 13:22:57 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Fri Sep 28 16:48:43 2018 +0200 @@ -36,8 +36,10 @@ #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/Logging.h" +#include "../../Core/SerializationToolbox.h" #include "../ServerContext.h" #include "../ServerJobs/ResourceModificationJob.h" +#include "../ServerJobs/SplitStudyJob.h" #include #include @@ -652,6 +654,93 @@ } + static void SplitStudy(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + Json::Value request; + if (!call.ParseJsonRequest(request)) + { + // Bad JSON request + throw OrthancException(ErrorCode_BadFileFormat); + } + + const std::string study = call.GetUriComponent("id", ""); + int priority = Toolbox::GetJsonIntegerField(request, "Priority", 0); + + std::auto_ptr job(new SplitStudyJob(context, study)); + job->SetOrigin(call); + job->SetDescription("REST API"); + + std::vector series; + SerializationToolbox::ReadArrayOfStrings(series, request, "Series"); + + for (size_t i = 0; i < series.size(); i++) + { + job->AddSourceSeries(series[i]); + } + + static const char* KEEP_SOURCE = "KeepSource"; + if (request.isMember(KEEP_SOURCE)) + { + job->SetKeepSource(SerializationToolbox::ReadBoolean(request, KEEP_SOURCE)); + } + + static const char* REMOVE = "Remove"; + if (request.isMember(REMOVE)) + { + if (request[REMOVE].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + for (Json::Value::ArrayIndex i = 0; i < request[REMOVE].size(); i++) + { + if (request[REMOVE][i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + job->Remove(FromDcmtkBridge::ParseTag(request[REMOVE][i].asCString())); + } + } + } + + static const char* REPLACE = "Replace"; + if (request.isMember(REPLACE)) + { + if (request[REPLACE].type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value::Members tags = request[REPLACE].getMemberNames(); + + for (size_t i = 0; i < tags.size(); i++) + { + const Json::Value& value = request[REPLACE][tags[i]]; + + if (value.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + else + { + job->Replace(FromDcmtkBridge::ParseTag(tags[i]), value.asString()); + } + } + } + + std::string id; + context.GetJobsEngine().GetRegistry().Submit(id, job.release(), priority); + + Json::Value v; + v["ID"] = id; + call.GetOutput().AnswerJson(v); + } + + void OrthancRestApi::RegisterAnonymizeModify() { Register("/instances/{id}/modify", ModifyInstance); @@ -665,5 +754,7 @@ Register("/patients/{id}/anonymize", AnonymizeResource); Register("/tools/create-dicom", CreateDicom); + + Register("/studies/{id}/split", SplitStudy); } } diff -r 4ee3a759afea -r 99863d6245b2 OrthancServer/ServerJobs/SplitStudyJob.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp Fri Sep 28 16:48:43 2018 +0200 @@ -0,0 +1,309 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 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 . + **/ + + +#include "SplitStudyJob.h" + +#include "../../Core/Logging.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" + +namespace Orthanc +{ + void SplitStudyJob::CheckAllowedTag(const DicomTag& tag) const + { + if (allowedTags_.find(tag) == allowedTags_.end()) + { + LOG(ERROR) << "Cannot modify the following tag while splitting a study " + << "(not in the patient/study modules): " + << FromDcmtkBridge::GetTagName(tag, "") << " (" << tag.Format() << ")"; + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void SplitStudyJob::Setup(const std::string& sourceStudy) + { + SetPermissive(false); + + keepSource_ = false; + sourceStudy_ = sourceStudy; + targetStudyUid_ = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study); + + DicomTag::AddTagsForModule(allowedTags_, DicomModule_Patient); + DicomTag::AddTagsForModule(allowedTags_, DicomModule_Study); + allowedTags_.erase(DICOM_TAG_STUDY_INSTANCE_UID); + allowedTags_.erase(DICOM_TAG_SERIES_INSTANCE_UID); + + ResourceType type; + + if (!context_.GetIndex().LookupResourceType(type, sourceStudy) || + type != ResourceType_Study) + { + LOG(ERROR) << "Cannot split unknown study: " << sourceStudy; + throw OrthancException(ErrorCode_UnknownResource); + } + + std::list children; + context_.GetIndex().GetChildren(children, sourceStudy); + + for (std::list::const_iterator + it = children.begin(); it != children.end(); ++it) + { + sourceSeries_.insert(*it); + } + } + + + bool SplitStudyJob::HandleInstance(const std::string& instance) + { + /** + * Retrieve the DICOM instance to be modified + **/ + + std::auto_ptr modified; + + try + { + ServerContext::DicomCacheLocker locker(context_, instance); + modified.reset(locker.GetDicom().Clone(true)); + } + catch (OrthancException&) + { + LOG(WARNING) << "An instance was removed after the job was issued: " << instance; + return false; + } + + + /** + * Chose the target UIDs + **/ + + std::string series; + if (!modified->GetTagValue(series, DICOM_TAG_SERIES_INSTANCE_UID)) + { + throw OrthancException(ErrorCode_BadFileFormat); // Should never happen + } + + std::string targetSeriesUid; + SeriesUidMap::const_iterator found = targetSeries_.find(series); + + if (found == targetSeries_.end()) + { + // Choose a random SeriesInstanceUID for this series + targetSeriesUid = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series); + targetSeries_[series] = targetSeriesUid; + } + else + { + targetSeriesUid = found->second; + } + + + /** + * Apply user-specified modifications + **/ + + for (Removals::const_iterator it = removals_.begin(); + it != removals_.end(); ++it) + { + modified->Remove(*it); + } + + for (Replacements::const_iterator it = replacements_.begin(); + it != replacements_.end(); ++it) + { + modified->ReplacePlainString(it->first, it->second); + } + + + /** + * Store the new instance into Orthanc + **/ + + modified->ReplacePlainString(DICOM_TAG_STUDY_INSTANCE_UID, targetStudyUid_); + modified->ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, targetSeriesUid); + + if (targetStudy_.empty()) + { + targetStudy_ = modified->GetHasher().HashStudy(); + } + + DicomInstanceToStore toStore; + toStore.SetOrigin(origin_); + toStore.SetParsedDicomFile(*modified); + + std::string modifiedInstance; + if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success) + { + LOG(ERROR) << "Error while storing a modified instance " << instance; + return false; + } + + return true; + } + + + bool SplitStudyJob::HandleTrailingStep() + { + if (!keepSource_) + { + const size_t n = GetInstancesCount(); + + for (size_t i = 0; i < n; i++) + { + Json::Value tmp; + context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance); + } + } + + return true; + } + + + SplitStudyJob::SplitStudyJob(ServerContext& context, + const std::string& sourceStudy) : + SetOfInstancesJob(true /* with trailing step */), + context_(context) + { + Setup(sourceStudy); + } + + + SplitStudyJob::SplitStudyJob(ServerContext& context, + const Json::Value& serialized) : + SetOfInstancesJob(serialized), + context_(context) + { + //assert(HasTrailingStep()); + //Setup(); + } + + + void SplitStudyJob::SetOrigin(const DicomInstanceOrigin& origin) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + origin_ = origin; + } + } + + + void SplitStudyJob::SetOrigin(const RestApiCall& call) + { + SetOrigin(DicomInstanceOrigin::FromRest(call)); + } + + + void SplitStudyJob::AddSourceSeries(const std::string& series) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (sourceSeries_.find(series) == sourceSeries_.end()) + { + LOG(ERROR) << "This series does not belong to the study to be split: " << series; + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + // Add all the instances of the series as to be processed + std::list instances; + context_.GetIndex().GetChildren(instances, series); + + for (std::list::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + AddInstance(*it); + } + } + } + + + void SplitStudyJob::SetKeepSource(bool keep) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + keepSource_ = keep; + } + + + void SplitStudyJob::Remove(const DicomTag& tag) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + CheckAllowedTag(tag); + removals_.insert(tag); + } + + + void SplitStudyJob::Replace(const DicomTag& tag, + const std::string& value) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + CheckAllowedTag(tag); + replacements_[tag] = value; + } + + + void SplitStudyJob::GetPublicContent(Json::Value& value) + { + SetOfInstancesJob::GetPublicContent(value); + + if (!targetStudy_.empty()) + { + value["TargetStudy"] = targetStudy_; + } + + value["TargetStudyUID"] = targetStudyUid_; + } + + + bool SplitStudyJob::Serialize(Json::Value& target) + { + return true; + } +} diff -r 4ee3a759afea -r 99863d6245b2 OrthancServer/ServerJobs/SplitStudyJob.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/SplitStudyJob.h Fri Sep 28 16:48:43 2018 +0200 @@ -0,0 +1,109 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 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 . + **/ + + +#pragma once + +#include "../../Core/JobsEngine/SetOfInstancesJob.h" + +#include "../ServerContext.h" + +namespace Orthanc +{ + class SplitStudyJob : public SetOfInstancesJob + { + private: + typedef std::map SeriesUidMap; + typedef std::map Replacements; + typedef std::set Removals; + + + ServerContext& context_; + std::string sourceStudy_; + std::set sourceSeries_; + bool keepSource_; + std::string targetStudy_; + std::string targetStudyUid_; + SeriesUidMap targetSeries_; + std::set allowedTags_; + DicomInstanceOrigin origin_; + Replacements replacements_; + Removals removals_; + + void CheckAllowedTag(const DicomTag& tag) const; + + void Setup(const std::string& sourceStudy); + + protected: + virtual bool HandleInstance(const std::string& instance); + + virtual bool HandleTrailingStep(); + + public: + SplitStudyJob(ServerContext& context, + const std::string& sourceStudy); + + SplitStudyJob(ServerContext& context, + const Json::Value& serialized); + + void SetOrigin(const DicomInstanceOrigin& origin); + + void SetOrigin(const RestApiCall& call); + + void AddSourceSeries(const std::string& series); + + bool IsKeepSource() const + { + return keepSource_; + } + + void SetKeepSource(bool keep); + + void Remove(const DicomTag& tag); + + void Replace(const DicomTag& tag, + const std::string& value); + + virtual void Stop(JobStopReason reason) + { + } + + virtual void GetJobType(std::string& target) + { + target = "SplitStudyJob"; + } + + virtual void GetPublicContent(Json::Value& value); + + virtual bool Serialize(Json::Value& target); + }; +}