# HG changeset patch # User Sebastien Jodogne # Date 1624458203 -7200 # Node ID 783f8a048035d3f810fc2bc49299e28df83429c9 # Parent 816a9ecc6ea13c4d91a6a97f993d1e8987be0416# Parent 758fe3ffb336c078faeeecdb047d37e82705c86a integration mainline->openssl-3.x diff -r 816a9ecc6ea1 -r 783f8a048035 NEWS --- a/NEWS Tue Jun 22 10:40:28 2021 +0200 +++ b/NEWS Wed Jun 23 16:23:23 2021 +0200 @@ -41,6 +41,8 @@ - GET /patients/{id}/module, GET /patients/{id}/patient-module - GET /series/{id}/module, GET /studies/{id}/module, GET /instances/{id}/module - POST /tools/find +* "/studies/{id}/split" accepts "Instances" parameter to split instances instead of series +* "/studies/{id}/merge" accepts instances inside its "Resources" parameter Maintenance ----------- diff -r 816a9ecc6ea1 -r 783f8a048035 OrthancServer/Resources/RunCppCheck.sh --- a/OrthancServer/Resources/RunCppCheck.sh Tue Jun 22 10:40:28 2021 +0200 +++ b/OrthancServer/Resources/RunCppCheck.sh Wed Jun 23 16:23:23 2021 +0200 @@ -19,12 +19,14 @@ stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:72 stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:384 stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:386 +stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:51 +stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:201 syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:50 syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:131 syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:321 uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:427 unreadVariable:../../OrthancFramework/Sources/FileStorage/StorageAccessor.cpp -unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1121 +unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1123 unusedFunction useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:89 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:97 diff -r 816a9ecc6ea1 -r 783f8a048035 OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue Jun 22 10:40:28 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Jun 23 16:23:23 2021 +0200 @@ -38,6 +38,24 @@ "Starting with Orthanc 1.9.4, paths to subsequences can be provided using the "\ "same syntax as the `dcmodify` command-line tool (wildcards are supported as well)." + +static const char* const CONTENT = "Content"; +static const char* const FORCE = "Force"; +static const char* const INSTANCES = "Instances"; +static const char* const INTERPRET_BINARY_TAGS = "InterpretBinaryTags"; +static const char* const KEEP = "Keep"; +static const char* const KEEP_PRIVATE_TAGS = "KeepPrivateTags"; +static const char* const KEEP_SOURCE = "KeepSource"; +static const char* const PARENT = "Parent"; +static const char* const PRIVATE_CREATOR = "PrivateCreator"; +static const char* const REMOVE = "Remove"; +static const char* const REPLACE = "Replace"; +static const char* const RESOURCES = "Resources"; +static const char* const SERIES = "Series"; +static const char* const TAGS = "Tags"; +static const char* const TRANSCODE = "Transcode"; + + namespace Orthanc { // Modification of DICOM instances ------------------------------------------ @@ -54,23 +72,23 @@ { // Check out "DicomModification::ParseModifyRequest()" call.GetDocumentation() - .SetRequestField("Transcode", RestApiCallDocumentation::Type_String, + .SetRequestField(TRANSCODE, RestApiCallDocumentation::Type_String, "Transcode the DICOM instances to the provided DICOM transfer syntax: " "https://book.orthanc-server.com/faq/transcoding.html", false) - .SetRequestField("Force", RestApiCallDocumentation::Type_Boolean, + .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean, "Allow the modification of tags related to DICOM identifiers, at the risk of " "breaking the DICOM model of the real world", false) .SetRequestField("RemovePrivateTags", RestApiCallDocumentation::Type_Boolean, "Remove the private tags from the DICOM instances (defaults to `false`)", false) - .SetRequestField("Replace", RestApiCallDocumentation::Type_JsonObject, + .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject, "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false) - .SetRequestField("Remove", RestApiCallDocumentation::Type_JsonListOfStrings, + .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings, "List of tags that must be removed from the DICOM instances. " INFO_SUBSEQUENCES, false) - .SetRequestField("Keep", RestApiCallDocumentation::Type_JsonListOfStrings, + .SetRequestField(KEEP, RestApiCallDocumentation::Type_JsonListOfStrings, "Keep the original value of the specified tags, to be chosen among the `StudyInstanceUID`, " "`SeriesInstanceUID` and `SOPInstanceUID` tags. Avoid this feature as much as possible, " "as this breaks the DICOM model of the real world.", false) - .SetRequestField("PrivateCreator", RestApiCallDocumentation::Type_String, + .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String, "The private creator to be used for private tags in `Replace`", false); } @@ -79,21 +97,21 @@ { // Check out "DicomModification::ParseAnonymizationRequest()" call.GetDocumentation() - .SetRequestField("Force", RestApiCallDocumentation::Type_Boolean, + .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean, "Allow the modification of tags related to DICOM identifiers, at the risk of " "breaking the DICOM model of the real world", false) .SetRequestField("DicomVersion", RestApiCallDocumentation::Type_String, "Version of the DICOM standard to be used for anonymization. Check out " "configuration option `DeidentifyLogsDicomVersion` for possible values.", false) - .SetRequestField("KeepPrivateTags", RestApiCallDocumentation::Type_Boolean, + .SetRequestField(KEEP_PRIVATE_TAGS, RestApiCallDocumentation::Type_Boolean, "Keep the private tags from the DICOM instances (defaults to `false`)", false) - .SetRequestField("Replace", RestApiCallDocumentation::Type_JsonObject, + .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject, "Associative array to change the value of some DICOM tags in the DICOM instances. " INFO_SUBSEQUENCES, false) - .SetRequestField("Remove", RestApiCallDocumentation::Type_JsonListOfStrings, + .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings, "List of additional tags to be removed from the DICOM instances. " INFO_SUBSEQUENCES, false) - .SetRequestField("Keep", RestApiCallDocumentation::Type_JsonListOfStrings, + .SetRequestField(KEEP, RestApiCallDocumentation::Type_JsonListOfStrings, "List of DICOM tags whose value must not be destroyed by the anonymization. " INFO_SUBSEQUENCES, false) - .SetRequestField("PrivateCreator", RestApiCallDocumentation::Type_String, + .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String, "The private creator to be used for private tags in `Replace`", false); } @@ -241,7 +259,6 @@ modification.SetLevel(DetectModifyLevel(modification)); - static const char* TRANSCODE = "Transcode"; if (request.isMember(TRANSCODE)) { std::string s = SerializationToolbox::ReadString(request, TRANSCODE); @@ -293,7 +310,6 @@ static void SetKeepSource(CleaningInstancesJob& job, const Json::Value& body) { - static const char* KEEP_SOURCE = "KeepSource"; if (body.isMember(KEEP_SOURCE)) { job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE)); @@ -325,7 +341,6 @@ job->SetOrigin(call); SetKeepSource(*job, body); - static const char* TRANSCODE = "Transcode"; if (body.isMember(TRANSCODE)) { job->SetTranscode(SerializationToolbox::ReadString(body, TRANSCODE)); @@ -365,7 +380,7 @@ const Json::Value& body) { std::set resources; - SerializationToolbox::ReadSetOfStrings(resources, body, "Resources"); + SerializationToolbox::ReadSetOfStrings(resources, body, RESOURCES); SubmitModificationJob(modification, isAnonymization, call, body, ResourceType_Instance /* arbitrary value, unused */, @@ -414,7 +429,7 @@ call.GetDocumentation() .SetTag("System") .SetSummary("Modify a set of resources") - .SetRequestField("Resources", RestApiCallDocumentation::Type_JsonListOfStrings, + .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings, "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true) .SetDescription("Start a job that will modify all the DICOM patients, studies, series or instances " "whose identifiers are provided in the `Resources` field.") @@ -472,7 +487,7 @@ call.GetDocumentation() .SetTag("System") .SetSummary("Anonymize a set of resources") - .SetRequestField("Resources", RestApiCallDocumentation::Type_JsonListOfStrings, + .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings, "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true) .SetDescription("Start a job that will anonymize all the DICOM patients, studies, series or instances " "whose identifiers are provided in the `Resources` field.") @@ -626,16 +641,16 @@ } else if (content[i].type() == Json::objectValue) { - if (!content[i].isMember("Content")) + if (!content[i].isMember(CONTENT)) { throw OrthancException(ErrorCode_CreateDicomNoPayload); } - payload = &content[i]["Content"]; + payload = &content[i][CONTENT]; - if (content[i].isMember("Tags")) + if (content[i].isMember(TAGS)) { - InjectTags(*dicom, content[i]["Tags"], decodeBinaryTags, privateCreator, force); + InjectTags(*dicom, content[i][TAGS], decodeBinaryTags, privateCreator, force); } } @@ -677,13 +692,7 @@ static void CreateDicomV2(RestApiPostCall& call, const Json::Value& request) { - static const char* const CONTENT = "Content"; - static const char* const FORCE = "Force"; - static const char* const INTERPRET_BINARY_TAGS = "InterpretBinaryTags"; - static const char* const PARENT = "Parent"; - static const char* const PRIVATE_CREATOR = "PrivateCreator"; static const char* const SPECIFIC_CHARACTER_SET_2 = "SpecificCharacterSet"; - static const char* const TAGS = "Tags"; static const char* const TYPE = "Type"; static const char* const VALUE = "Value"; @@ -757,7 +766,7 @@ // Choose the same encoding as the parent resource { - static const char* SPECIFIC_CHARACTER_SET = "0008,0005"; + static const char* const SPECIFIC_CHARACTER_SET = "0008,0005"; if (siblingTags.isMember(SPECIFIC_CHARACTER_SET)) { @@ -936,25 +945,25 @@ .SetTag("System") .SetSummary("Create one DICOM instance") .SetDescription("Create one DICOM instance, and store it into Orthanc") - .SetRequestField("Tags", RestApiCallDocumentation::Type_JsonObject, + .SetRequestField(TAGS, RestApiCallDocumentation::Type_JsonObject, "Associative array containing the tags of the new instance to be created", true) - .SetRequestField("Content", RestApiCallDocumentation::Type_String, + .SetRequestField(CONTENT, RestApiCallDocumentation::Type_String, "This field can be used to embed an image (pixel data) or a PDF inside the created DICOM instance. " "The PNG image, the JPEG image or the PDF file must be provided using their " "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme). " "This field can possibly contain a JSON array, in which case a DICOM series is created " "containing one DICOM instance for each item in the `Content` field.", false) - .SetRequestField("Parent", RestApiCallDocumentation::Type_String, + .SetRequestField(PARENT, RestApiCallDocumentation::Type_String, "If present, the newly created instance will be attached to the parent DICOM resource " "whose Orthanc identifier is contained in this field. The DICOM tags of the parent " "modules in the DICOM hierarchy will be automatically copied to the newly created instance.", false) - .SetRequestField("InterpretBinaryTags", RestApiCallDocumentation::Type_Boolean, + .SetRequestField(INTERPRET_BINARY_TAGS, RestApiCallDocumentation::Type_Boolean, "If some value in the `Tags` associative array is formatted according to some " "[data URI scheme encoding](https://en.wikipedia.org/wiki/Data_URI_scheme), " "whether this value is decoded to a binary value or kept as such (`true` by default)", false) - .SetRequestField("PrivateCreator", RestApiCallDocumentation::Type_String, + .SetRequestField(PRIVATE_CREATOR, RestApiCallDocumentation::Type_String, "The private creator to be used for private tags in `Tags`", false) - .SetRequestField("Force", RestApiCallDocumentation::Type_Boolean, + .SetRequestField(FORCE, RestApiCallDocumentation::Type_Boolean, "Avoid the consistency checks for the DICOM tags that enforce the DICOM model of the real-world. " "You can notably use this flag if you need to manually set the tags `StudyInstanceUID`, " "`SeriesInstanceUID`, or `SOPInstanceUID`. Be careful with this feature.", false) @@ -970,7 +979,7 @@ throw OrthancException(ErrorCode_BadRequest); } - if (request.isMember("Tags")) + if (request.isMember(TAGS)) { CreateDicomV2(call, request); } @@ -995,22 +1004,25 @@ .SetTag("Studies") .SetSummary("Split study") .SetDescription("Start a new job so as to split the DICOM study whose Orthanc identifier is provided in the URL, " - "by taking some of its children series out of it and putting them into a brand new study (this " - "new study is created by setting the `StudyInstanceUID` tag to a random identifier): " + "by taking some of its children series or instances out of it and putting them into a brand new study " + "(this new study is created by setting the `StudyInstanceUID` tag to a random identifier): " "https://book.orthanc-server.com/users/anonymization.html#splitting") .SetUriArgument("id", "Orthanc identifier of the study of interest") - .SetRequestField("Series", RestApiCallDocumentation::Type_JsonListOfStrings, - "The list of series to be separated from the parent study (mandatory option). " - "These series must all be children of the same source study, that is specified in the URI.", true) - .SetRequestField("Replace", RestApiCallDocumentation::Type_JsonObject, + .SetRequestField(SERIES, RestApiCallDocumentation::Type_JsonListOfStrings, + "The list of series to be separated from the parent study. " + "These series must all be children of the same source study, that is specified in the URI.", false) + .SetRequestField(REPLACE, RestApiCallDocumentation::Type_JsonObject, "Associative array to change the value of some DICOM tags in the new study. " "These tags must be part of the \"Patient Module Attributes\" or the \"General Study " "Module Attributes\", as specified by the DICOM 2011 standard in Tables C.7-1 and C.7-3.", false) - .SetRequestField("Remove", RestApiCallDocumentation::Type_JsonListOfStrings, + .SetRequestField(REMOVE, RestApiCallDocumentation::Type_JsonListOfStrings, "List of tags that must be removed in the new study (from the same modules as in the `Replace` option)", false) - .SetRequestField("KeepSource", RestApiCallDocumentation::Type_Boolean, - "If set to `true`, instructs Orthanc to keep a copy of the original series in the source study. " - "By default, the original series are deleted from Orthanc.", false); + .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean, + "If set to `true`, instructs Orthanc to keep a copy of the original series/instances in the source study. " + "By default, the original series/instances are deleted from Orthanc.", false) + .SetRequestField(INSTANCES, RestApiCallDocumentation::Type_JsonListOfStrings, + "The list of instances to be separated from the parent study. " + "These instances must all be children of the same source study, that is specified in the URI.", false); return; } @@ -1028,19 +1040,40 @@ std::unique_ptr job(new SplitStudyJob(context, study)); job->SetOrigin(call); - std::vector series; - SerializationToolbox::ReadArrayOfStrings(series, request, "Series"); + bool ok = false; + if (request.isMember(SERIES)) + { + std::vector series; + SerializationToolbox::ReadArrayOfStrings(series, request, SERIES); + + for (size_t i = 0; i < series.size(); i++) + { + job->AddSourceSeries(series[i]); + ok = true; + } + } - for (size_t i = 0; i < series.size(); i++) + if (request.isMember(INSTANCES)) { - job->AddSourceSeries(series[i]); + std::vector instances; + SerializationToolbox::ReadArrayOfStrings(instances, request, INSTANCES); + + for (size_t i = 0; i < instances.size(); i++) + { + job->AddSourceInstance(instances[i]); + ok = true; + } } + + if (!ok) + { + throw OrthancException(ErrorCode_BadRequest, "Both the \"Series\" and the \"Instances\" fields are missing"); + } job->AddTrailingStep(); SetKeepSource(*job, request); - static const char* REMOVE = "Remove"; if (request.isMember(REMOVE)) { if (request[REMOVE].type() != Json::arrayValue) @@ -1061,7 +1094,6 @@ } } - static const char* REPLACE = "Replace"; if (request.isMember(REPLACE)) { if (request[REPLACE].type() != Json::objectValue) @@ -1099,13 +1131,13 @@ call.GetDocumentation() .SetTag("Studies") .SetSummary("Merge study") - .SetDescription("Start a new job so as to move some DICOM series into the DICOM study whose Orthanc identifier " + .SetDescription("Start a new job so as to move some DICOM resources into the DICOM study whose Orthanc identifier " "is provided in the URL: https://book.orthanc-server.com/users/anonymization.html#merging") .SetUriArgument("id", "Orthanc identifier of the study of interest") - .SetRequestField("Resources", RestApiCallDocumentation::Type_JsonListOfStrings, - "The list of DICOM resources (patients, studies, series, and/or instances) to be merged " + .SetRequestField(RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings, + "The list of DICOM resources (studies, series, and/or instances) to be merged " "into the study of interest (mandatory option)", true) - .SetRequestField("KeepSource", RestApiCallDocumentation::Type_Boolean, + .SetRequestField(KEEP_SOURCE, RestApiCallDocumentation::Type_Boolean, "If set to `true`, instructs Orthanc to keep a copy of the original resources in their source study. " "By default, the original resources are deleted from Orthanc.", false); return; @@ -1126,7 +1158,7 @@ job->SetOrigin(call); std::vector resources; - SerializationToolbox::ReadArrayOfStrings(resources, request, "Resources"); + SerializationToolbox::ReadArrayOfStrings(resources, request, RESOURCES); for (size_t i = 0; i < resources.size(); i++) { diff -r 816a9ecc6ea1 -r 783f8a048035 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Jun 22 10:40:28 2021 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Jun 23 16:23:23 2021 +0200 @@ -2535,7 +2535,7 @@ DicomToJsonFormat format_; public: - FindVisitor(DicomToJsonFormat format) : + explicit FindVisitor(DicomToJsonFormat format) : isComplete_(false), format_(format) { @@ -3101,8 +3101,29 @@ } + static void AddMetadata(Json::Value& target, + ServerIndex& index, + const std::string& resource, + ResourceType level) + { + target = Json::objectValue; + + std::map content; + index.GetAllMetadata(content, resource, level); + + for (std::map::const_iterator + it = content.begin(); it != content.end(); ++it) + { + target[EnumerationToString(it->first)] = it->second; + } + } + + static void BulkContent(RestApiPostCall& call) { + static const char* const LEVEL = "Level"; + static const char* const METADATA = "Metadata"; + if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human); @@ -3112,10 +3133,12 @@ .SetSummary("Describe a set of instances") .SetRequestField("Resources", RestApiCallDocumentation::Type_JsonListOfStrings, "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true) - .SetRequestField("Level", RestApiCallDocumentation::Type_String, + .SetRequestField(LEVEL, RestApiCallDocumentation::Type_String, "This optional argument specifies the level of interest (can be `Patient`, `Study`, `Series` or " "`Instance`). Orthanc will loop over the items inside `Resources`, and explorer upward or " "downward in the DICOM hierarchy in order to find the level of interest.", false) + .SetRequestField(METADATA, RestApiCallDocumentation::Type_Boolean, + "If set to `true` (default value), the metadata associated with the resources will also be retrieved.", false) .SetDescription("Get the content all the DICOM patients, studies, series or instances " "whose identifiers are provided in the `Resources` field, in one single call."); return; @@ -3130,10 +3153,14 @@ } else { - static const char* const LEVEL = "Level"; - const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human); + bool metadata = true; + if (request.isMember(METADATA)) + { + metadata = SerializationToolbox::ReadBoolean(request, METADATA); + } + ServerIndex& index = OrthancRestApi::GetIndex(call); Json::Value answer = Json::arrayValue; @@ -3178,9 +3205,9 @@ if (type == level) { for (std::set::const_iterator - it = children.begin(); it != children.end(); ++it) + it2 = children.begin(); it2 != children.end(); ++it2) { - interest.insert(*it); + interest.insert(*it2); } break; // done @@ -3231,6 +3258,11 @@ Json::Value item; if (index.ExpandResource(item, *it, level, format)) { + if (metadata) + { + AddMetadata(item[METADATA], index, *it, level); + } + answer.append(item); } } @@ -3244,11 +3276,16 @@ for (std::list::const_iterator it = resources.begin(); it != resources.end(); ++it) { - ResourceType type; + ResourceType level; Json::Value item; - if (index.LookupResourceType(type, *it) && - index.ExpandResource(item, *it, type, format)) + if (index.LookupResourceType(level, *it) && + index.ExpandResource(item, *it, level, format)) { + if (metadata) + { + AddMetadata(item[METADATA], index, *it, level); + } + answer.append(item); } else diff -r 816a9ecc6ea1 -r 783f8a048035 OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp --- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp Tue Jun 22 10:40:28 2021 +0200 +++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp Wed Jun 23 16:23:23 2021 +0200 @@ -30,10 +30,20 @@ namespace Orthanc { + static void RegisterSeries(std::map& target, + const std::string& series) + { + // Generate a target SeriesInstanceUID for this series + if (target.find(series) == target.end()) + { + target[series] = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series); + } + } + + void MergeStudyJob::AddSourceSeriesInternal(const std::string& series) { - // Generate a target SeriesInstanceUID for this series - seriesUidMap_[series] = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series); + RegisterSeries(seriesUidMap_, series); // Add all the instances of the series as to be processed std::list instances; @@ -228,7 +238,7 @@ } - void MergeStudyJob::AddSource(const std::string& studyOrSeries) + void MergeStudyJob::AddSource(const std::string& publicId) { ResourceType level; @@ -236,28 +246,31 @@ { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else if (!GetContext().GetIndex().LookupResourceType(level, studyOrSeries)) + else if (!GetContext().GetIndex().LookupResourceType(level, publicId)) { throw OrthancException(ErrorCode_UnknownResource, - "Cannot find this resource: " + studyOrSeries); + "Cannot find this resource: " + publicId); } else { switch (level) { case ResourceType_Study: - AddSourceStudyInternal(studyOrSeries); + AddSourceStudyInternal(publicId); break; case ResourceType_Series: - AddSourceSeries(studyOrSeries); + AddSourceSeries(publicId); + break; + + case ResourceType_Instance: + AddSourceInstance(publicId); break; default: throw OrthancException(ErrorCode_UnknownResource, - "This resource is neither a study, nor a series: " + - studyOrSeries + " is a " + - std::string(EnumerationToString(level))); + "This resource is neither a study, nor a series, nor an instance: " + + publicId + " is a " + std::string(EnumerationToString(level))); } } } @@ -310,6 +323,34 @@ } + void MergeStudyJob::AddSourceInstance(const std::string& instance) + { + std::string parentStudy, parentSeries; + + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (!GetContext().GetIndex().LookupParent(parentSeries, instance, ResourceType_Series) || + !GetContext().GetIndex().LookupParent(parentStudy, parentSeries, ResourceType_Study)) + { + throw OrthancException(ErrorCode_UnknownResource, + "This resource is not an instance: " + instance); + } + else if (parentStudy == targetStudy_) + { + throw OrthancException(ErrorCode_UnknownResource, + "Cannot merge instance " + instance + + " into its parent study " + targetStudy_); + } + else + { + RegisterSeries(seriesUidMap_, parentSeries); + AddInstance(instance); + } + } + + void MergeStudyJob::GetPublicContent(Json::Value& value) { CleaningInstancesJob::GetPublicContent(value); diff -r 816a9ecc6ea1 -r 783f8a048035 OrthancServer/Sources/ServerJobs/MergeStudyJob.h --- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.h Tue Jun 22 10:40:28 2021 +0200 +++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.h Wed Jun 23 16:23:23 2021 +0200 @@ -47,6 +47,10 @@ void AddSourceStudyInternal(const std::string& study); + // Make setter methods private to prevent incorrect calls + using SetOfInstancesJob::AddParentResource; + using SetOfInstancesJob::AddInstance; + protected: virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE; @@ -62,12 +66,14 @@ return targetStudy_; } - void AddSource(const std::string& studyOrSeries); + void AddSource(const std::string& publicId); void AddSourceStudy(const std::string& study); void AddSourceSeries(const std::string& series); + void AddSourceInstance(const std::string& instance); // New in Orthanc 1.9.4 + void SetOrigin(const DicomInstanceOrigin& origin); void SetOrigin(const RestApiCall& call); diff -r 816a9ecc6ea1 -r 783f8a048035 OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp --- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp Tue Jun 22 10:40:28 2021 +0200 +++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp Wed Jun 23 16:23:23 2021 +0200 @@ -180,6 +180,17 @@ } + static void RegisterSeries(std::map& target, + const std::string& series) + { + // Generate a target SeriesInstanceUID for this series + if (target.find(series) == target.end()) + { + target[series] = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series); + } + } + + void SplitStudyJob::AddSourceSeries(const std::string& series) { std::string parent; @@ -196,8 +207,7 @@ } else { - // Generate a target SeriesInstanceUID for this series - seriesUidMap_[series] = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series); + RegisterSeries(seriesUidMap_, series); // Add all the instances of the series as to be processed std::list instances; @@ -212,6 +222,29 @@ } + void SplitStudyJob::AddSourceInstance(const std::string& instance) + { + std::string study, series; + + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (!GetContext().GetIndex().LookupParent(series, instance, ResourceType_Series) || + !GetContext().GetIndex().LookupParent(study, series, ResourceType_Study) || + study != sourceStudy_) + { + throw OrthancException(ErrorCode_UnknownResource, + "This instance does not belong to the study to be split: " + instance); + } + else + { + RegisterSeries(seriesUidMap_, series); + AddInstance(instance); + } + } + + bool SplitStudyJob::LookupTargetSeriesUid(std::string& uid, const std::string& series) const { diff -r 816a9ecc6ea1 -r 783f8a048035 OrthancServer/Sources/ServerJobs/SplitStudyJob.h --- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.h Tue Jun 22 10:40:28 2021 +0200 +++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.h Wed Jun 23 16:23:23 2021 +0200 @@ -48,6 +48,10 @@ void CheckAllowedTag(const DicomTag& tag) const; void Setup(); + + // Make setter methods private to prevent incorrect calls + using SetOfInstancesJob::AddParentResource; + using SetOfInstancesJob::AddInstance; protected: virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE; @@ -76,6 +80,8 @@ void AddSourceSeries(const std::string& series); + void AddSourceInstance(const std::string& instance); // New in Orthanc 1.9.4 + bool LookupTargetSeriesUid(std::string& uid, const std::string& series) const;