# HG changeset patch # User Alain Mazy # Date 1650894657 -7200 # Node ID 8b95fc86b8d9f2bda584d8c9651fc78db4cf68f6 # Parent 5e7404f23fa8db79e58f73ae79ad87013d3fcf19# Parent f316413027fddba1ed0e37a486209d107ff7eed3 merged more-tags -> default diff -r 5e7404f23fa8 -r 8b95fc86b8d9 NEWS --- a/NEWS Wed Apr 13 10:51:22 2022 +0200 +++ b/NEWS Mon Apr 25 15:50:57 2022 +0200 @@ -4,8 +4,43 @@ General ------- +* New configuration "ExtraMainDicomTags" to store more tags in the Index DB + to speed up, e.g, building C-Find, dicom-web or tools/find answers +* New sample plugin: "Housekeeper" that will re-construct the DB/Storage + when it detects there is room for improvements, e.g: + - if files were stored with a version of Orthanc prior to 1.9.1, + the storage might still contain dicom-as-json files that are not needed + anymore -> it will remove them + - if "ExtraMainDicomTags" has changed. + - if "StorageCompression" has chagned. +* New configuration "Warnings" to enable/disable individual warnings that can + be identified by a W0XX prefix in the logs. + These warnings have been added: + - W001_TagsBeingReadFromStorage + - W002_InconsistentDicomTagsInDb +* C-Find and QIDO-RS can now return the InstanceAvailability tag. Value is + always "ONLINE" * Improved decoding of US Images with Implicit VR. - +* Speed-up handling of DicomModalitiesInStudy in C-Find and tools/find queries. + +REST API +-------- + +* API version upgraded to 17 +* new options in tools/find: + - "RequestedTags" (to use together with "Expand": true) contains a list of tags + that you'll receive in the "RequestedTags" field in the answers. These tags + may be tags from the MainDicomTags in DB, from the DICOM file or 'computed' + like ModalitiesInStudy. Check the new configuration "ExtraMainDicomTags" and + "Warnings" to optimize your queries. +* new query argument "requestedTags" in all API routes listing resources: + - /patients, /patients/../studies, /patients/../series, /patients/../instances + - /studies, /studies/../series, /studies/../instances + - /series, /series/../instances + - /instances +* new field "MainDicomTags" in the /system route response to list the tags that + are saved in DB +* new field "StorageCompression" reported in the /system route response Version 1.10.1 (2022-03-23) diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Mon Apr 25 15:50:57 2022 +0200 @@ -38,7 +38,7 @@ # Version of the Orthanc API, can be retrieved from "/system" URI in # order to check whether new URI endpoints are available even if using # the mainline version of Orthanc -set(ORTHANC_API_VERSION "16") +set(ORTHANC_API_VERSION "17") ##################################################################### diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Resources/CodeGeneration/ErrorCodes.json --- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Mon Apr 25 15:50:57 2022 +0200 @@ -245,6 +245,11 @@ "Name": "Revision", "Description": "A bad revision number was provided, which might indicate conflict between multiple writers" }, + { + "Code": 44, + "Name": "MainDicomTagsMultiplyDefined", + "Description": "A main DICOM Tag has been defined multiple times for the same resource level" + }, diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/DicomFormat/DicomArray.cpp --- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -69,6 +69,16 @@ } } + void DicomArray::GetTags(std::set& tags) const + { + tags.clear(); + + for (size_t i = 0; i < elements_.size(); i++) + { + tags.insert(elements_[i]->GetTag()); + } + + } void DicomArray::Print(FILE* fp) const { diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/DicomFormat/DicomArray.h --- a/OrthancFramework/Sources/DicomFormat/DicomArray.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomArray.h Mon Apr 25 15:50:57 2022 +0200 @@ -46,6 +46,8 @@ const DicomElement& GetElement(size_t i) const; + void GetTags(std::set& tags) const; + void Print(FILE* fp) const; // For debugging only }; } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/DicomFormat/DicomMap.cpp --- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -26,6 +26,7 @@ #include #include +#include #include "../Compatibility.h" #include "../Endianness.h" @@ -44,26 +45,38 @@ const DicomTag tag_; const char* name_; }; + typedef std::vector MainDicomTags; + } - static const MainDicomTag PATIENT_MAIN_DICOM_TAGS[] = + + // WARNING: the DEFAULT list of main dicom tags below are the list as they + // were in Orthanc 1.10 before we introduced the dynamic main dicom tags. + // This list has not changed since Orthanc 1.4.2 and had a single change since + // Orthanc 0.9.5. + // These lists have a specific signature. When a resource does not have + // the metadata "MainDicomTagsSignature", we'll assume that they were stored + // with an Orthanc prior to 1.11. It is therefore very important that you never + // change these lists ! + + static const MainDicomTag DEFAULT_PATIENT_MAIN_DICOM_TAGS[] = { // { DicomTag(0x0010, 0x1010), "PatientAge" }, // { DicomTag(0x0010, 0x1040), "PatientAddress" }, - { DicomTag(0x0010, 0x0010), "PatientName" }, - { DicomTag(0x0010, 0x0030), "PatientBirthDate" }, - { DicomTag(0x0010, 0x0040), "PatientSex" }, - { DicomTag(0x0010, 0x1000), "OtherPatientIDs" }, + { DICOM_TAG_PATIENT_NAME, "PatientName" }, + { DICOM_TAG_PATIENT_BIRTH_DATE, "PatientBirthDate" }, + { DICOM_TAG_PATIENT_SEX, "PatientSex" }, + { DICOM_TAG_OTHER_PATIENT_IDS, "OtherPatientIDs" }, { DICOM_TAG_PATIENT_ID, "PatientID" } }; - - static const MainDicomTag STUDY_MAIN_DICOM_TAGS[] = + + static const MainDicomTag DEFAULT_STUDY_MAIN_DICOM_TAGS[] = { // { DicomTag(0x0010, 0x1020), "PatientSize" }, // { DicomTag(0x0010, 0x1030), "PatientWeight" }, { DICOM_TAG_STUDY_DATE, "StudyDate" }, - { DicomTag(0x0008, 0x0030), "StudyTime" }, - { DicomTag(0x0020, 0x0010), "StudyID" }, + { DICOM_TAG_STUDY_TIME, "StudyTime" }, + { DICOM_TAG_STUDY_ID, "StudyID" }, { DICOM_TAG_STUDY_DESCRIPTION, "StudyDescription" }, { DICOM_TAG_ACCESSION_NUMBER, "AccessionNumber" }, { DICOM_TAG_STUDY_INSTANCE_UID, "StudyInstanceUID" }, @@ -74,20 +87,20 @@ { DICOM_TAG_REQUESTING_PHYSICIAN, "RequestingPhysician" }, { DICOM_TAG_REFERRING_PHYSICIAN_NAME, "ReferringPhysicianName" } }; - - static const MainDicomTag SERIES_MAIN_DICOM_TAGS[] = + + static const MainDicomTag DEFAULT_SERIES_MAIN_DICOM_TAGS[] = { // { DicomTag(0x0010, 0x1080), "MilitaryRank" }, - { DicomTag(0x0008, 0x0021), "SeriesDate" }, - { DicomTag(0x0008, 0x0031), "SeriesTime" }, + { DICOM_TAG_SERIES_DATE, "SeriesDate" }, + { DICOM_TAG_SERIES_TIME, "SeriesTime" }, { DICOM_TAG_MODALITY, "Modality" }, - { DicomTag(0x0008, 0x0070), "Manufacturer" }, - { DicomTag(0x0008, 0x1010), "StationName" }, + { DICOM_TAG_MANUFACTURER, "Manufacturer" }, + { DICOM_TAG_STATION_NAME, "StationName" }, { DICOM_TAG_SERIES_DESCRIPTION, "SeriesDescription" }, - { DicomTag(0x0018, 0x0015), "BodyPartExamined" }, - { DicomTag(0x0018, 0x0024), "SequenceName" }, - { DicomTag(0x0018, 0x1030), "ProtocolName" }, - { DicomTag(0x0020, 0x0011), "SeriesNumber" }, + { DICOM_TAG_BODY_PART_EXAMINED, "BodyPartExamined" }, + { DICOM_TAG_SEQUENCE_NAME, "SequenceName" }, + { DICOM_TAG_PROTOCOL_NAME, "ProtocolName" }, + { DICOM_TAG_SERIES_NUMBER, "SeriesNumber" }, { DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES, "CardiacNumberOfImages" }, { DICOM_TAG_IMAGES_IN_ACQUISITION, "ImagesInAcquisition" }, { DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS, "NumberOfTemporalPositions" }, @@ -103,12 +116,12 @@ { DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION, "AcquisitionDeviceProcessingDescription" }, { DICOM_TAG_CONTRAST_BOLUS_AGENT, "ContrastBolusAgent" } }; - - static const MainDicomTag INSTANCE_MAIN_DICOM_TAGS[] = + + static const MainDicomTag DEFAULT_INSTANCE_MAIN_DICOM_TAGS[] = { - { DicomTag(0x0008, 0x0012), "InstanceCreationDate" }, - { DicomTag(0x0008, 0x0013), "InstanceCreationTime" }, - { DicomTag(0x0020, 0x0012), "AcquisitionNumber" }, + { DICOM_TAG_INSTANCE_CREATION_DATE, "InstanceCreationDate" }, + { DICOM_TAG_INSTANCE_CREATION_TIME, "InstanceCreationTime" }, + { DICOM_TAG_ACQUISITION_NUMBER, "AcquisitionNumber" }, { DICOM_TAG_IMAGE_INDEX, "ImageIndex" }, { DICOM_TAG_INSTANCE_NUMBER, "InstanceNumber" }, { DICOM_TAG_NUMBER_OF_FRAMES, "NumberOfFrames" }, @@ -130,57 +143,6 @@ }; - static void LoadMainDicomTags(const MainDicomTag*& tags, - size_t& size, - ResourceType level) - { - switch (level) - { - case ResourceType_Patient: - tags = PATIENT_MAIN_DICOM_TAGS; - size = sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag); - break; - - case ResourceType_Study: - tags = STUDY_MAIN_DICOM_TAGS; - size = sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag); - break; - - case ResourceType_Series: - tags = SERIES_MAIN_DICOM_TAGS; - size = sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag); - break; - - case ResourceType_Instance: - tags = INSTANCE_MAIN_DICOM_TAGS; - size = sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - - - static void LoadMainDicomTags(std::map& target, - ResourceType level) - { - const MainDicomTag* tags = NULL; - size_t size; - LoadMainDicomTags(tags, size, level); - - assert(tags != NULL && - size != 0); - - for (size_t i = 0; i < size; i++) - { - assert(target.find(tags[i].tag_) == target.end()); - - target[tags[i].tag_] = tags[i].name_; - } - } - - namespace { class DicomTag2 : public DicomTag @@ -199,23 +161,170 @@ } - static void LoadMainDicomTags(std::map& target, - ResourceType level) + class DicomMap::MainDicomTagsConfiguration { - const MainDicomTag* tags = NULL; - size_t size; - LoadMainDicomTags(tags, size, level); + private: + friend DicomMap; + + // we keep many "copies" of the same data to guarantee quick access to organized data + // and avoid rebuilding it all the time. + std::map > mainDicomTagsByTag_; + std::map > mainDicomTagsByName_; + std::map > mainDicomTagsByLevel_; + std::set allMainDicomTags_; + + std::map signatures_; + std::map defaultSignatures_; + + MainDicomTagsConfiguration() + { + ResetDefaultMainDicomTags(); + } + + void ResetDefaultMainDicomTags() + { + mainDicomTagsByTag_.clear(); + mainDicomTagsByName_.clear(); + mainDicomTagsByLevel_.clear(); + allMainDicomTags_.clear(); + + // by default, initialize with the previous static list (up to 1.10.0) + LoadDefaultMainDicomTags(ResourceType_Patient); + LoadDefaultMainDicomTags(ResourceType_Study); + LoadDefaultMainDicomTags(ResourceType_Series); + LoadDefaultMainDicomTags(ResourceType_Instance); + + defaultSignatures_[ResourceType_Patient] = signatures_[ResourceType_Patient]; + defaultSignatures_[ResourceType_Study] = signatures_[ResourceType_Study]; + defaultSignatures_[ResourceType_Series] = signatures_[ResourceType_Series]; + defaultSignatures_[ResourceType_Instance] = signatures_[ResourceType_Instance]; + } + + std::string ComputeSignature(const std::set& tags) + { + // std::set are sorted by default (which is important for us !) + std::set tagsIds; + for (std::set::const_iterator it = tags.begin(); it != tags.end(); it++) + { + tagsIds.insert(it->Format()); + } + + std::string signatureText = boost::algorithm::join(tagsIds, ";"); + + return signatureText; + } + + void LoadDefaultMainDicomTags(ResourceType level) + { + assert(mainDicomTagsByTag_.find(level) == mainDicomTagsByTag_.end()); + + const MainDicomTag* tags = NULL; + size_t size; + + switch (level) + { + case ResourceType_Patient: + tags = DEFAULT_PATIENT_MAIN_DICOM_TAGS; + size = sizeof(DEFAULT_PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag); + break; + + case ResourceType_Study: + tags = DEFAULT_STUDY_MAIN_DICOM_TAGS; + size = sizeof(DEFAULT_STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag); + break; + + case ResourceType_Series: + tags = DEFAULT_SERIES_MAIN_DICOM_TAGS; + size = sizeof(DEFAULT_SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag); + break; + + case ResourceType_Instance: + tags = DEFAULT_INSTANCE_MAIN_DICOM_TAGS; + size = sizeof(DEFAULT_INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag); + break; - assert(tags != NULL && - size != 0); + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + assert(tags != NULL && + size != 0); + + for (size_t i = 0; i < size; i++) + { + AddMainDicomTag(tags[i].tag_, tags[i].name_, level); + } - for (size_t i = 0; i < size; i++) + } + + public: + // Singleton pattern + static MainDicomTagsConfiguration& GetInstance() + { + static MainDicomTagsConfiguration parameters; + return parameters; + } + + void AddMainDicomTag(const DicomTag& tag, const std::string& name, ResourceType level) { - assert(target.find(tags[i].name_) == target.end()); - - target[tags[i].name_] = DicomTag2(tags[i].tag_); + if (mainDicomTagsByTag_[level].find(tag) != mainDicomTagsByTag_[level].end()) + { + throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, tag.Format() + " is already defined"); + } + + if (mainDicomTagsByName_[level].find(name) != mainDicomTagsByName_[level].end()) + { + throw OrthancException(ErrorCode_MainDicomTagsMultiplyDefined, name + " is already defined"); + } + + mainDicomTagsByTag_[level][tag] = name; + mainDicomTagsByName_[level][name] = DicomTag2(tag); + mainDicomTagsByLevel_[level].insert(tag); + allMainDicomTags_.insert(tag); + signatures_[level] = ComputeSignature(GetMainDicomTagsByLevel(level)); + } + + const std::map& GetMainDicomTags(ResourceType level) const + { + assert(mainDicomTagsByTag_.find(level) != mainDicomTagsByTag_.end()); + + return mainDicomTagsByTag_.at(level); } - } + + const std::map& GetMainDicomTagsByName(ResourceType level) const + { + assert(mainDicomTagsByName_.find(level) != mainDicomTagsByName_.end()); + + return mainDicomTagsByName_.at(level); + } + + const std::set& GetMainDicomTagsByLevel(ResourceType level) const + { + assert(mainDicomTagsByLevel_.find(level) != mainDicomTagsByLevel_.end()); + + return mainDicomTagsByLevel_.at(level); + } + + const std::set& GetAllMainDicomTags() const + { + return allMainDicomTags_; + } + + const std::string& GetMainDicomTagsSignature(ResourceType level) + { + assert(signatures_.find(level) != signatures_.end()); + + return signatures_[level]; + } + + const std::string& GetDefaultMainDicomTagsSignature(ResourceType level) + { + assert(defaultSignatures_.find(level) != defaultSignatures_.end()); + + return defaultSignatures_[level]; + } + + }; void DicomMap::SetValueInternal(uint16_t group, @@ -294,16 +403,17 @@ } - static void ExtractTags(DicomMap& result, + // MORE_TAGS: TODO: we can probably remove the std::string from MainDicomTags (not used here !!!) + static void ExtractTagsInternal(DicomMap& result, const DicomMap::Content& source, - const MainDicomTag* tags, - size_t count) + const std::map& mainDicomTags) { result.Clear(); - for (unsigned int i = 0; i < count; i++) + for (std::map::const_iterator itmt = mainDicomTags.begin(); + itmt != mainDicomTags.end(); itmt++) { - DicomMap::Content::const_iterator it = source.find(tags[i].tag_); + DicomMap::Content::const_iterator it = source.find(itmt->first); if (it != source.end()) { result.SetValue(it->first, *it->second /* value will be cloned */); @@ -311,25 +421,45 @@ } } + void DicomMap::ExtractTags(DicomMap& result, const std::set& tags) const + { + result.Clear(); + + for (std::set::const_iterator itmt = tags.begin(); + itmt != tags.end(); itmt++) + { + DicomMap::Content::const_iterator it = content_.find(*itmt); + if (it != content_.end()) + { + result.SetValue(it->first, *it->second /* value will be cloned */); + } + } + } + + void DicomMap::ExtractResourceInformation(DicomMap& result, ResourceType level) const + { + const std::map& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(level); + ExtractTagsInternal(result, content_, mainDicomTags); + } void DicomMap::ExtractPatientInformation(DicomMap& result) const { - ExtractTags(result, content_, PATIENT_MAIN_DICOM_TAGS, sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag)); + ExtractResourceInformation(result, ResourceType_Patient); } void DicomMap::ExtractStudyInformation(DicomMap& result) const { - ExtractTags(result, content_, STUDY_MAIN_DICOM_TAGS, sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag)); + ExtractResourceInformation(result, ResourceType_Study); } void DicomMap::ExtractSeriesInformation(DicomMap& result) const { - ExtractTags(result, content_, SERIES_MAIN_DICOM_TAGS, sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag)); + ExtractResourceInformation(result, ResourceType_Series); } void DicomMap::ExtractInstanceInformation(DicomMap& result) const { - ExtractTags(result, content_, INSTANCE_MAIN_DICOM_TAGS, sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag)); + ExtractResourceInformation(result, ResourceType_Instance); } @@ -415,25 +545,27 @@ static void SetupFindTemplate(DicomMap& result, - const MainDicomTag* tags, - size_t count) + const std::map& mainDicomTags) { result.Clear(); - for (size_t i = 0; i < count; i++) + for (std::map::const_iterator itmt = mainDicomTags.begin(); + itmt != mainDicomTags.end(); itmt++) { - result.SetValue(tags[i].tag_, "", false); + result.SetValue(itmt->first, "", false); } } void DicomMap::SetupFindPatientTemplate(DicomMap& result) { - SetupFindTemplate(result, PATIENT_MAIN_DICOM_TAGS, sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag)); + const std::map& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Patient); + SetupFindTemplate(result, mainDicomTags); } void DicomMap::SetupFindStudyTemplate(DicomMap& result) { - SetupFindTemplate(result, STUDY_MAIN_DICOM_TAGS, sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag)); + const std::map& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Study); + SetupFindTemplate(result, mainDicomTags); result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false); result.SetValue(DICOM_TAG_PATIENT_ID, "", false); @@ -446,7 +578,8 @@ void DicomMap::SetupFindSeriesTemplate(DicomMap& result) { - SetupFindTemplate(result, SERIES_MAIN_DICOM_TAGS, sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag)); + const std::map& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Series); + SetupFindTemplate(result, mainDicomTags); result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false); result.SetValue(DICOM_TAG_PATIENT_ID, "", false); result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false); @@ -468,7 +601,8 @@ void DicomMap::SetupFindInstanceTemplate(DicomMap& result) { - SetupFindTemplate(result, INSTANCE_MAIN_DICOM_TAGS, sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag)); + const std::map& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(ResourceType_Instance); + SetupFindTemplate(result, mainDicomTags); result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "", false); result.SetValue(DICOM_TAG_PATIENT_ID, "", false); result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "", false); @@ -488,19 +622,8 @@ bool DicomMap::IsMainDicomTag(const DicomTag& tag, ResourceType level) { - const MainDicomTag *tags = NULL; - size_t size; - LoadMainDicomTags(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - if (tags[i].tag_ == tag) - { - return true; - } - } - - return false; + const std::map& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(level); + return mainDicomTags.find(tag) != mainDicomTags.end(); } bool DicomMap::IsMainDicomTag(const DicomTag& tag) @@ -511,36 +634,124 @@ IsMainDicomTag(tag, ResourceType_Instance)); } + static bool IsGenericComputedTag(const DicomTag& tag) + { + return tag == DICOM_TAG_RETRIEVE_URL || + tag == DICOM_TAG_RETRIEVE_AE_TITLE; + } - void DicomMap::GetMainDicomTagsInternal(std::set& result, ResourceType level) + bool DicomMap::IsComputedTag(const DicomTag& tag) + { + return (IsComputedTag(tag, ResourceType_Patient) || + IsComputedTag(tag, ResourceType_Study) || + IsComputedTag(tag, ResourceType_Series) || + IsComputedTag(tag, ResourceType_Instance) || + IsGenericComputedTag(tag)); + } + + bool DicomMap::IsComputedTag(const DicomTag& tag, ResourceType level) { - const MainDicomTag *tags = NULL; - size_t size; - LoadMainDicomTags(tags, size, level); + + switch (level) + { + case ResourceType_Patient: + return ( + tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES || + tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES || + tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES + ); + case ResourceType_Study: + return ( + tag == DICOM_TAG_MODALITIES_IN_STUDY || + tag == DICOM_TAG_SOP_CLASSES_IN_STUDY || + tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES || + tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES + ); + case ResourceType_Series: + return ( + tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES + ); + case ResourceType_Instance: + return ( + tag == DICOM_TAG_INSTANCE_AVAILABILITY + ); + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } - for (size_t i = 0; i < size; i++) + bool DicomMap::HasOnlyComputedTags(const std::set& tags) + { + if (tags.size() == 0) + { + return false; + } + + for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) { - result.insert(tags[i].tag_); + if (!IsComputedTag(*it)) + { + return false; + } } + return true; + } + + bool DicomMap::HasComputedTags(const std::set& tags) + { + for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + if (IsComputedTag(*it)) + { + return true; + } + } + + return false; + } + + bool DicomMap::HasComputedTags(const std::set& tags, ResourceType level) + { + for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + if (IsComputedTag(*it, level)) + { + return true; + } + } + return false; } - void DicomMap::GetMainDicomTags(std::set& result, ResourceType level) + const std::set& DicomMap::GetMainDicomTags(ResourceType level) + { + return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(level); + } + + const std::set& DicomMap::GetAllMainDicomTags() { - result.clear(); - GetMainDicomTagsInternal(result, level); + return DicomMap::MainDicomTagsConfiguration::GetInstance().GetAllMainDicomTags(); + } + + void DicomMap::AddMainDicomTag(const DicomTag& tag, const std::string& name, ResourceType level) + { + DicomMap::MainDicomTagsConfiguration::GetInstance().AddMainDicomTag(tag, name, level); } - - void DicomMap::GetMainDicomTags(std::set& result) + void DicomMap::ResetDefaultMainDicomTags() { - result.clear(); - GetMainDicomTagsInternal(result, ResourceType_Patient); - GetMainDicomTagsInternal(result, ResourceType_Study); - GetMainDicomTagsInternal(result, ResourceType_Series); - GetMainDicomTagsInternal(result, ResourceType_Instance); + DicomMap::MainDicomTagsConfiguration::GetInstance().ResetDefaultMainDicomTags(); } + const std::string& DicomMap::GetMainDicomTagsSignature(ResourceType level) + { + return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsSignature(level); + } + + const std::string& DicomMap::GetDefaultMainDicomTagsSignature(ResourceType level) + { + return DicomMap::MainDicomTagsConfiguration::GetInstance().GetDefaultMainDicomTagsSignature(level); + } void DicomMap::GetTags(std::set& tags) const { @@ -1201,21 +1412,18 @@ void DicomMap::MergeMainDicomTags(const DicomMap& other, ResourceType level) { - const MainDicomTag* tags = NULL; - size_t size = 0; + const std::map& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(level); - LoadMainDicomTags(tags, size, level); - assert(tags != NULL && size > 0); - - for (size_t i = 0; i < size; i++) + for (std::map::const_iterator itmt = mainDicomTags.begin(); + itmt != mainDicomTags.end(); itmt++) { - Content::const_iterator found = other.content_.find(tags[i].tag_); + Content::const_iterator found = other.content_.find(itmt->first); if (found != other.content_.end() && - content_.find(tags[i].tag_) == content_.end()) + content_.find(itmt->first) == content_.end()) { assert(found->second != NULL); - content_[tags[i].tag_] = found->second->Clone(); + content_[itmt->first] = found->second->Clone(); } } } @@ -1233,14 +1441,11 @@ bool DicomMap::HasOnlyMainDicomTags() const { - // TODO - Speed up possible by making this std::set a global variable - - std::set mainDicomTags; - GetMainDicomTags(mainDicomTags); + const std::set& allMainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetAllMainDicomTags(); for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it) { - if (mainDicomTags.find(it->first) == mainDicomTags.end()) + if (allMainDicomTags.find(it->first) == allMainDicomTags.end()) { return false; } @@ -1475,8 +1680,7 @@ void DicomMap::DumpMainDicomTags(Json::Value& target, ResourceType level) const { - std::map mainTags; // TODO - Create a singleton to hold this map - LoadMainDicomTags(mainTags, level); + const std::map& mainDicomTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTags(level); target = Json::objectValue; @@ -1487,9 +1691,9 @@ if (!it->second->IsBinary() && !it->second->IsNull()) { - std::map::const_iterator found = mainTags.find(it->first); + std::map::const_iterator found = mainDicomTags.find(it->first); - if (found != mainTags.end()) + if (found != mainDicomTags.end()) { target[found->second] = it->second->GetContent(); } @@ -1506,8 +1710,7 @@ throw OrthancException(ErrorCode_BadFileFormat); } - std::map mainTags; // TODO - Create a singleton to hold this map - LoadMainDicomTags(mainTags, level); + const std::map& mainTags = DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByName(level); Json::Value::Members members = source.getMemberNames(); for (size_t i = 0; i < members.size(); i++) diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/DicomFormat/DicomMap.h --- a/OrthancFramework/Sources/DicomFormat/DicomMap.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h Mon Apr 25 15:50:57 2022 +0200 @@ -31,18 +31,27 @@ #include #include +#if ORTHANC_BUILD_UNIT_TESTS == 1 +# include +#endif + namespace Orthanc { class ORTHANC_PUBLIC DicomMap : public boost::noncopyable { public: typedef std::map Content; - + private: + class MainDicomTagsConfiguration; friend class DicomArray; friend class FromDcmtkBridge; friend class ParsedDicomFile; +#if ORTHANC_BUILD_UNIT_TESTS == 1 + friend class DicomMapMainTagsTests; +#endif + Content content_; // Warning: This takes the ownership of "value" @@ -50,8 +59,8 @@ uint16_t element, DicomValue* value); - static void GetMainDicomTagsInternal(std::set& result, - ResourceType level); + // used for unit tests only + static void ResetDefaultMainDicomTags(); public: ~DicomMap(); @@ -109,6 +118,10 @@ void ExtractInstanceInformation(DicomMap& result) const; + void ExtractResourceInformation(DicomMap& result, ResourceType level) const; + + void ExtractTags(DicomMap& result, const std::set& tags) const; + static void SetupFindPatientTemplate(DicomMap& result); static void SetupFindStudyTemplate(DicomMap& result); @@ -124,9 +137,28 @@ static bool IsMainDicomTag(const DicomTag& tag); - static void GetMainDicomTags(std::set& result, ResourceType level); + static bool IsComputedTag(const DicomTag& tag, ResourceType level); + + static bool IsComputedTag(const DicomTag& tag); + + static bool HasOnlyComputedTags(const std::set& tags); + + static bool HasComputedTags(const std::set& tags, ResourceType level); + + static bool HasComputedTags(const std::set& tags); - static void GetMainDicomTags(std::set& result); + static const std::set& GetMainDicomTags(ResourceType level); + + // returns a string uniquely identifying the list of main dicom tags for a level + static const std::string& GetMainDicomTagsSignature(ResourceType level); + + static const std::string& GetDefaultMainDicomTagsSignature(ResourceType level); + + static const std::set& GetAllMainDicomTags(); + + // adds a main dicom tag to the definition of main dicom tags for each level. + // this should be done once at startup before you use MainDicomTags methods + static void AddMainDicomTag(const DicomTag& tag, const std::string& name, ResourceType level); void GetTags(std::set& tags) const; diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/DicomFormat/DicomTag.h --- a/OrthancFramework/Sources/DicomFormat/DicomTag.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h Mon Apr 25 15:50:57 2022 +0200 @@ -117,6 +117,7 @@ static const DicomTag DICOM_TAG_QUERY_RETRIEVE_LEVEL(0x0008, 0x0052); static const DicomTag DICOM_TAG_MODALITIES_IN_STUDY(0x0008, 0x0061); static const DicomTag DICOM_TAG_RETRIEVE_AE_TITLE(0x0008, 0x0054); + static const DicomTag DICOM_TAG_INSTANCE_AVAILABILITY(0x0008, 0x0056); // Tags for images static const DicomTag DICOM_TAG_COLUMNS(0x0028, 0x0011); @@ -166,8 +167,11 @@ static const DicomTag DICOM_TAG_PATIENT_SEX(0x0010, 0x0040); static const DicomTag DICOM_TAG_LATERALITY(0x0020, 0x0060); static const DicomTag DICOM_TAG_BODY_PART_EXAMINED(0x0018, 0x0015); + static const DicomTag DICOM_TAG_SEQUENCE_NAME(0x0018, 0x0024); + static const DicomTag DICOM_TAG_PROTOCOL_NAME(0x0018, 0x1030); static const DicomTag DICOM_TAG_VIEW_POSITION(0x0018, 0x5101); static const DicomTag DICOM_TAG_MANUFACTURER(0x0008, 0x0070); + static const DicomTag DICOM_TAG_STATION_NAME(0x0008, 0x1010); static const DicomTag DICOM_TAG_PATIENT_ORIENTATION(0x0020, 0x0020); static const DicomTag DICOM_TAG_PATIENT_COMMENTS(0x0010, 0x4000); static const DicomTag DICOM_TAG_PATIENT_SPECIES_DESCRIPTION(0x0010, 0x2201); @@ -176,6 +180,7 @@ static const DicomTag DICOM_TAG_PER_FRAME_FUNCTIONAL_GROUP_SEQUENCE(0x5200, 0x9230); static const DicomTag DICOM_TAG_PIXEL_VALUE_TRANSFORMATION_SEQUENCE(0x0028, 0x9145); static const DicomTag DICOM_TAG_FRAME_VOI_LUT_SEQUENCE(0x0028, 0x9132); + static const DicomTag DICOM_TAG_ACQUISITION_NUMBER(0x0020, 0x0012); // Tags used within the Stone of Orthanc static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009); @@ -225,4 +230,8 @@ static const DicomTag DICOM_TAG_OFFSET_OF_REFERENCED_LOWER_LEVEL_DIRECTORY_ENTITY(0x0004, 0x1420); static const DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE(0x0004, 0x1511); static const DicomTag DICOM_TAG_REFERENCED_FILE_ID(0x0004, 0x1500); + + // Tags for DicomWeb + static const Orthanc::DicomTag DICOM_TAG_RETRIEVE_URL(0x0008, 0x1190); + } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -1296,7 +1296,7 @@ else { CLOG(INFO, DICOM) << "Unknown DICOM tag: \"" << name << "\""; - throw OrthancException(ErrorCode_UnknownDicomTag); + throw OrthancException(ErrorCode_UnknownDicomTag, name, false); } #endif } @@ -1311,6 +1311,65 @@ return fields.HasTag(ParseTag(tagName)); } + void FromDcmtkBridge::FormatListOfTags(std::string& output, const std::set& tags) + { + std::set values; + for (std::set::const_iterator it = tags.begin(); + it != tags.end(); it++) + { + values.insert(it->Format()); + } + + Toolbox::JoinStrings(output, values, ";"); + } + + void FromDcmtkBridge::FormatListOfTags(Json::Value& output, const std::set& tags) + { + output = Json::arrayValue; + for (std::set::const_iterator it = tags.begin(); + it != tags.end(); it++) + { + output.append(it->Format()); + } + } + + // parses a list like "0010,0010;PatientBirthDate;0020,0020" + void FromDcmtkBridge::ParseListOfTags(std::set& result, const std::string& source) + { + result.clear(); + + std::vector tokens; + Toolbox::TokenizeString(tokens, source, ';'); + + for (std::vector::const_iterator it = tokens.begin(); + it != tokens.end(); it++) + { + if (it->size() > 0) + { + DicomTag tag = FromDcmtkBridge::ParseTag(*it); + result.insert(tag); + } + } + } + + + void FromDcmtkBridge::ParseListOfTags(std::set& result, const Json::Value& source) + { + result.clear(); + + if (!source.isArray()) + { + throw OrthancException(ErrorCode_BadRequest, "List of tags is not an array"); + } + + for (Json::ArrayIndex i = 0; i < source.size(); i++) + { + const std::string& value = source[i].asString(); + DicomTag tag = FromDcmtkBridge::ParseTag(value); + result.insert(tag); + } + } + const DicomValue &FromDcmtkBridge::GetValue(const DicomMap &fields, const std::string &tagName) { diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Mon Apr 25 15:50:57 2022 +0200 @@ -175,6 +175,15 @@ static DicomTag ParseTag(const std::string& name); + // parses a list like "0010,0010;PatientBirthDate;0020,0020" + static void ParseListOfTags(std::set& result, const std::string& source); + + static void ParseListOfTags(std::set& result, const Json::Value& source); + + static void FormatListOfTags(std::string& output, const std::set& tags); + + static void FormatListOfTags(Json::Value& output, const std::set& tags); + static bool HasTag(const DicomMap& fields, const std::string& tagName); diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/Enumerations.cpp --- a/OrthancFramework/Sources/Enumerations.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/Enumerations.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -55,7 +55,7 @@ static const char* const MIME_ICO = "image/x-icon"; // This function is autogenerated by the script - // "Resources/GenerateErrorCodes.py" + // "Resources/CodeGeneration/GenerateErrorCodes.py" const char* EnumerationToString(ErrorCode error) { switch (error) @@ -195,6 +195,9 @@ case ErrorCode_Revision: return "A bad revision number was provided, which might indicate conflict between multiple writers"; + case ErrorCode_MainDicomTagsMultiplyDefined: + return "A main DICOM Tag has been defined multiple times for the same resource level"; + case ErrorCode_SQLiteNotOpened: return "SQLite: The database is not opened"; @@ -2159,7 +2162,7 @@ // This function is autogenerated by the script - // "Resources/GenerateErrorCodes.py" + // "Resources/CodeGeneration/GenerateErrorCodes.py" HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error) { switch (error) diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/Enumerations.h --- a/OrthancFramework/Sources/Enumerations.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/Enumerations.h Mon Apr 25 15:50:57 2022 +0200 @@ -91,7 +91,7 @@ }; // This enumeration is autogenerated by the script - // "Resources/GenerateErrorCodes.py" + // "Resources/CodeGeneration/GenerateErrorCodes.py" enum ErrorCode { ErrorCode_InternalError = -1 /*!< Internal error */, @@ -139,6 +139,7 @@ ErrorCode_BadRange = 41 /*!< Incorrect range request */, ErrorCode_DatabaseCannotSerialize = 42 /*!< Database could not serialize access due to concurrent update, the transaction should be retried */, ErrorCode_Revision = 43 /*!< A bad revision number was provided, which might indicate conflict between multiple writers */, + ErrorCode_MainDicomTagsMultiplyDefined = 44 /*!< A main DICOM Tag has been defined multiple times for the same resource level */, ErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, ErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, ErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/SQLite/OrthancSQLiteException.h --- a/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h Mon Apr 25 15:50:57 2022 +0200 @@ -52,7 +52,7 @@ { namespace SQLite { - // Auto-generated by "Resources/GenerateErrorCodes.py" + // Auto-generated by "Resources/CodeGeneration/GenerateErrorCodes.py" enum ErrorCode { ErrorCode_ParameterOutOfRange, @@ -83,7 +83,7 @@ { } - // Auto-generated by "Resources/GenerateErrorCodes.py" + // Auto-generated by "Resources/CodeGeneration/GenerateErrorCodes.py" static const char* EnumerationToString(ErrorCode code) { switch (code) diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/Toolbox.cpp --- a/OrthancFramework/Sources/Toolbox.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/Toolbox.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -56,6 +56,7 @@ #include #include +#include #include #include @@ -1032,6 +1033,21 @@ } + void Toolbox::JoinStrings(std::string& result, + std::set& source, + const char* separator) + { + result = boost::algorithm::join(source, separator); + } + + void JoinStrings(std::string& result, + std::vector& source, + const char* separator) + { + result = boost::algorithm::join(source, separator); + } + + #if ORTHANC_ENABLE_PUGIXML == 1 class ChunkedBufferWriter : public pugi::xml_writer { diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/Sources/Toolbox.h --- a/OrthancFramework/Sources/Toolbox.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/Sources/Toolbox.h Mon Apr 25 15:50:57 2022 +0200 @@ -185,6 +185,64 @@ const std::string& source, char separator); + static void JoinStrings(std::string& result, + std::set& source, + const char* separator); + + static void JoinStrings(std::string& result, + std::vector& source, + const char* separator); + + // returns true if all element of 'needles' are found in 'haystack' + template static bool IsSetInSet(const std::set& needles, const std::set& haystack) + { + for (typename std::set::const_iterator it = needles.begin(); + it != needles.end(); it++) + { + if (haystack.count(*it) == 0) + { + return false; + } + } + + return true; + } + + // returns the set of elements from 'needles' that are not in 'haystack' + template static size_t GetMissingsFromSet(std::set& missings, const std::set& needles, const std::set& haystack) + { + missings.clear(); + + for (typename std::set::const_iterator it = needles.begin(); + it != needles.end(); it++) + { + if (haystack.count(*it) == 0) + { + missings.insert(*it); + } + } + + return missings.size(); + } + + template static void AppendSets(std::set& target, const std::set& toAppend) + { + for (typename std::set::const_iterator it = toAppend.begin(); + it != toAppend.end(); it++) + { + target.insert(*it); + } + } + + template static void RemoveSets(std::set& target, const std::set& toRemove) + { + for (typename std::set::const_iterator it = toRemove.begin(); + it != toRemove.end(); it++) + { + target.erase(*it); + } + } + #if ORTHANC_ENABLE_PUGIXML == 1 static void JsonToXml(std::string& target, const Json::Value& source, diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/UnitTestsSources/DicomMapTests.cpp --- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -45,44 +45,130 @@ using namespace Orthanc; -TEST(DicomMap, MainTags) + +namespace Orthanc { - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID)); - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient)); - ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study)); + // The namespace is necessary because of FRIEND_TEST + // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members + + class DicomMapMainTagsTests : public ::testing::Test + { + public: + DicomMapMainTagsTests() + { + } + + virtual void SetUp() ORTHANC_OVERRIDE + { + DicomMap::ResetDefaultMainDicomTags(); + } + + virtual void TearDown() ORTHANC_OVERRIDE + { + DicomMap::ResetDefaultMainDicomTags(); + } + }; - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER)); - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); - ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID)); + TEST_F(DicomMapMainTagsTests, MainTags) + { + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient)); + ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study)); + + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID)); - std::set s; - DicomMap::GetMainDicomTags(s); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID)); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID)); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER)); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID)); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); + { + const std::set& s = DicomMap::GetAllMainDicomTags(); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); + } + + { + const std::set& s = DicomMap::GetMainDicomTags(ResourceType_Patient); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID)); + } + + { + const std::set& s = DicomMap::GetMainDicomTags(ResourceType_Study); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + } - DicomMap::GetMainDicomTags(s, ResourceType_Patient); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID)); - ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID)); + { + const std::set& s = DicomMap::GetMainDicomTags(ResourceType_Series); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + } + + { + const std::set& s = DicomMap::GetMainDicomTags(ResourceType_Instance); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + } + } - DicomMap::GetMainDicomTags(s, ResourceType_Study); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID)); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER)); - ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + TEST_F(DicomMapMainTagsTests, AddMainTags) + { + DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, "BitsAllocated", ResourceType_Instance); + + { + const std::set& s = DicomMap::GetMainDicomTags(ResourceType_Instance); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_BITS_ALLOCATED)); + ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); + } + { + const std::set& s = DicomMap::GetMainDicomTags(ResourceType_Series); + ASSERT_TRUE(s.end() == s.find(DICOM_TAG_BITS_ALLOCATED)); + } + + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_BITS_ALLOCATED)); + ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_BITS_ALLOCATED, ResourceType_Instance)); + + // adding the same tag should throw + ASSERT_THROW(DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, "BitsAllocated", ResourceType_Instance), OrthancException); - DicomMap::GetMainDicomTags(s, ResourceType_Series); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID)); - ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + // adding another tag with same name should throw + ASSERT_THROW(DicomMap::AddMainDicomTag(DICOM_TAG_BITS_STORED, "BitsAllocated", ResourceType_Instance), OrthancException); + } + + TEST_F(DicomMapMainTagsTests, Signatures) + { + std::string defaultPatientSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Patient); + std::string defaultStudySignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Study); + std::string defaultSeriesSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Series); + std::string defaultInstanceSignature = DicomMap::GetDefaultMainDicomTagsSignature(ResourceType_Instance); + + ASSERT_NE(defaultInstanceSignature, defaultPatientSignature); + ASSERT_NE(defaultSeriesSignature, defaultStudySignature); + ASSERT_NE(defaultSeriesSignature, defaultPatientSignature); - DicomMap::GetMainDicomTags(s, ResourceType_Instance); - ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID)); - ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID)); + std::string patientSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Patient); + std::string studySignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Study); + std::string seriesSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Series); + std::string instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance); + + // at start, default and current signature should be equal + ASSERT_EQ(defaultPatientSignature, patientSignature); + ASSERT_EQ(defaultStudySignature, studySignature); + ASSERT_EQ(defaultSeriesSignature, seriesSignature); + ASSERT_EQ(defaultInstanceSignature, instanceSignature); + + DicomMap::AddMainDicomTag(DICOM_TAG_BITS_ALLOCATED, "BitsAllocated", ResourceType_Instance); + instanceSignature = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance); + + ASSERT_NE(defaultInstanceSignature, instanceSignature); + } + } - TEST(DicomMap, Tags) { std::set s; @@ -157,9 +243,9 @@ // REFERENCE: DICOM PS3.3 2015c - Information Object Definitions // http://dicom.nema.org/medical/dicom/current/output/html/part03.html - std::set moduleTags, main; + std::set moduleTags; + const std::set& main = DicomMap::GetMainDicomTags(level); DicomTag::AddTagsForModule(moduleTags, module); - DicomMap::GetMainDicomTags(main, level); // The main dicom tags are a subset of the module for (std::set::const_iterator it = main.begin(); it != main.end(); ++it) @@ -470,6 +556,55 @@ } +TEST(DicomMap, ComputedTags) +{ + { + std::set tags; + + ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Study)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient)); + } + + { + std::set tags; + tags.insert(DICOM_TAG_ACCESSION_NUMBER); + + ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Study)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient)); + } + + { + std::set tags; + tags.insert(DICOM_TAG_MODALITIES_IN_STUDY); + tags.insert(DICOM_TAG_RETRIEVE_URL); + + ASSERT_TRUE(DicomMap::HasOnlyComputedTags(tags)); + ASSERT_TRUE(DicomMap::HasComputedTags(tags, ResourceType_Study)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance)); + } + + { + std::set tags; + tags.insert(DICOM_TAG_ACCESSION_NUMBER); + tags.insert(DICOM_TAG_MODALITIES_IN_STUDY); + + ASSERT_FALSE(DicomMap::HasOnlyComputedTags(tags)); + ASSERT_TRUE(DicomMap::HasComputedTags(tags, ResourceType_Study)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Patient)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Series)); + ASSERT_FALSE(DicomMap::HasComputedTags(tags, ResourceType_Instance)); + } + +} + TEST(DicomMap, RemoveBinary) { DicomMap b; @@ -622,8 +757,7 @@ { ResourceType level = static_cast(i); - std::set tags; - DicomMap::GetMainDicomTags(tags, level); + const std::set& tags = DicomMap::GetMainDicomTags(level); for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) { diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp --- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -357,6 +357,64 @@ } +TEST(FromDcmtkBridge, ParseListOfTags) +{ + {// nominal test + std::string source = "0010,0010;PatientBirthDate;0020,0020"; + std::set result; + FromDcmtkBridge::ParseListOfTags(result, source); + + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_NAME) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_BIRTH_DATE) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ORIENTATION) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ID) == result.end()); + + // serialize to string + std::string serialized; + FromDcmtkBridge::FormatListOfTags(serialized, result); + ASSERT_EQ("0010,0010;0010,0030;0020,0020", serialized); + } + + {// no tag + std::string source = ""; + std::set result; + FromDcmtkBridge::ParseListOfTags(result, source); + + ASSERT_EQ(0, result.size()); + } + + {// invalid tag + std::string source = "0010,0010;Patient-BirthDate;0020,0020"; + std::set result; + + ASSERT_THROW(FromDcmtkBridge::ParseListOfTags(result, source), OrthancException); + } + + {// duplicate tag only once + std::string source = "0010,0010;PatientName"; + std::set result; + + FromDcmtkBridge::ParseListOfTags(result, source); + + ASSERT_EQ(1, result.size()); + } + + {// Json + Json::Value source = Json::arrayValue; + source.append("0010,0010"); + source.append("PatientBirthDate"); + source.append("0020,0020"); + std::set result; + FromDcmtkBridge::ParseListOfTags(result, source); + + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_NAME) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_BIRTH_DATE) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ORIENTATION) != result.end()); + ASSERT_TRUE(result.find(DICOM_TAG_PATIENT_ID) == result.end()); + } + + +} static const DicomTag REFERENCED_STUDY_SEQUENCE(0x0008, 0x1110); static const DicomTag REFERENCED_PATIENT_SEQUENCE(0x0008, 0x1120); diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancFramework/UnitTestsSources/ToolboxTests.cpp --- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -209,3 +209,116 @@ std::unique_ptr > j(new SingleValueObject(42)); ASSERT_EQ(42, j->GetValue()); } + +TEST(Toolbox, IsSetInSet) +{ + { + std::set needles; + std::set haystack; + std::set missings; + + ASSERT_TRUE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(0, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + } + + { + std::set needles; + std::set haystack; + std::set missings; + + haystack.insert(5); + ASSERT_TRUE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(0, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + } + + { + std::set needles; + std::set haystack; + std::set missings; + + needles.insert(5); + haystack.insert(5); + ASSERT_TRUE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(0, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + } + + { + std::set needles; + std::set haystack; + std::set missings; + + needles.insert(5); + + ASSERT_FALSE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(1, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + ASSERT_TRUE(missings.count(5) == 1); + } + + { + std::set needles; + std::set haystack; + std::set missings; + + needles.insert(6); + haystack.insert(5); + ASSERT_FALSE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(1, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + ASSERT_TRUE(missings.count(6) == 1); + } + + { + std::set needles; + std::set haystack; + std::set missings; + + needles.insert(5); + needles.insert(6); + haystack.insert(5); + haystack.insert(6); + ASSERT_TRUE(Toolbox::IsSetInSet(needles, haystack)); + ASSERT_EQ(0, Toolbox::GetMissingsFromSet(missings, needles, haystack)); + } +} + +TEST(Toolbox, JoinStrings) +{ + { + std::set source; + std::string result; + + Toolbox::JoinStrings(result, source, ";"); + ASSERT_EQ("", result); + } + + { + std::set source; + source.insert("1"); + + std::string result; + + Toolbox::JoinStrings(result, source, ";"); + ASSERT_EQ("1", result); + } + + { + std::set source; + source.insert("2"); + source.insert("1"); + + std::string result; + + Toolbox::JoinStrings(result, source, ";"); + ASSERT_EQ("1;2", result); + } + + { + std::set source; + source.insert("2"); + source.insert("1"); + + std::string result; + + Toolbox::JoinStrings(result, source, "\\"); + ASSERT_EQ("1\\2", result); + } +} \ No newline at end of file diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/CMakeLists.txt Mon Apr 25 15:50:57 2022 +0200 @@ -59,6 +59,7 @@ SET(BUILD_RECOVER_COMPRESSED_FILE ON CACHE BOOL "Whether to build the companion tool to recover files compressed using Orthanc") SET(BUILD_SERVE_FOLDERS ON CACHE BOOL "Whether to build the ServeFolders plugin") SET(BUILD_CONNECTIVITY_CHECKS ON CACHE BOOL "Whether to build the ConnectivityChecks plugin") +SET(BUILD_HOUSEKEEPER ON CACHE BOOL "Whether to build the Housekeeper plugin") SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins") SET(UNIT_TESTS_WITH_HTTP_CONNEXIONS ON CACHE BOOL "Allow unit tests to make HTTP requests") @@ -315,6 +316,7 @@ -DHAS_ORTHANC_EXCEPTION=0 -DMODALITY_WORKLISTS_VERSION="${ORTHANC_VERSION}" -DSERVE_FOLDERS_VERSION="${ORTHANC_VERSION}" + -DDB_OPTIMIZER_VERSION="${ORTHANC_VERSION}" ) @@ -427,7 +429,7 @@ ##################################################################### if (ENABLE_PLUGINS AND - (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS)) + (BUILD_SERVE_FOLDERS OR BUILD_MODALITY_WORKLISTS OR BUILD_DB_OPTIMIZER)) add_library(ThirdPartyPlugins STATIC ${BOOST_SOURCES} ${JSONCPP_SOURCES} @@ -605,6 +607,50 @@ ##################################################################### +## Build the "Housekeeper" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_DB_OPTIMIZER) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") +execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/WindowsResources.py + ${ORTHANC_VERSION} Housekeeper Housekeeper.dll "Sample Orthanc plugin to optimize/clean the DB/Storage" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/Housekeeper.rc + ) + +if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") +endif() + +list(APPEND HOUSEKEEPER_RESOURCES ${AUTOGENERATED_DIR}/Housekeeper.rc) +endif() + +add_library(Housekeeper SHARED +${CMAKE_SOURCE_DIR}/Plugins/Samples/Housekeeper/Plugin.cpp +${HOUSEKEEPER_RESOURCES} +) + +target_link_libraries(Housekeeper + ThirdPartyPlugins + ) + +set_target_properties( + Housekeeper PROPERTIES +VERSION ${ORTHANC_VERSION} +SOVERSION ${ORTHANC_VERSION} +) + +install( +TARGETS Housekeeper +RUNTIME DESTINATION lib # Destination for Windows +LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux +) +endif() + + +##################################################################### ## Build the companion tool to recover files compressed using Orthanc ##################################################################### diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Mon Apr 25 15:50:57 2022 +0200 @@ -244,6 +244,7 @@ OrthancPluginErrorCode_BadRange = 41 /*!< Incorrect range request */, OrthancPluginErrorCode_DatabaseCannotSerialize = 42 /*!< Database could not serialize access due to concurrent update, the transaction should be retried */, OrthancPluginErrorCode_Revision = 43 /*!< A bad revision number was provided, which might indicate conflict between multiple writers */, + OrthancPluginErrorCode_MainDicomTagsMultiplyDefined = 44 /*!< A main DICOM Tag has been defined multiple times for the same resource level */, OrthancPluginErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, OrthancPluginErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */, diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp --- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -1546,24 +1546,18 @@ " is required)"); } - - bool CheckMinimalOrthancVersion(unsigned int major, - unsigned int minor, - unsigned int revision) + bool CheckMinimalVersion(const char* version, + unsigned int major, + unsigned int minor, + unsigned int revision) { - if (!HasGlobalContext()) - { - LogError("Bad Orthanc context in the plugin"); - return false; - } - - if (!strcmp(GetGlobalContext()->orthancVersion, "mainline")) + if (!strcmp(version, "mainline")) { // Assume compatibility with the mainline return true; } - // Parse the version of the Orthanc core + // Parse the version int aa, bb, cc; if ( #ifdef _MSC_VER @@ -1571,7 +1565,7 @@ #else sscanf #endif - (GetGlobalContext()->orthancVersion, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 || + (version, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 || aa < 0 || bb < 0 || cc < 0) @@ -1595,7 +1589,6 @@ return false; } - // Check the minor version number assert(a == major); @@ -1623,6 +1616,21 @@ } + bool CheckMinimalOrthancVersion(unsigned int major, + unsigned int minor, + unsigned int revision) + { + if (!HasGlobalContext()) + { + LogError("Bad Orthanc context in the plugin"); + return false; + } + + return CheckMinimalVersion(GetGlobalContext()->orthancVersion, + major, minor, revision); + } + + #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0) const char* AutodetectMimeType(const std::string& path) { diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h --- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Mon Apr 25 15:50:57 2022 +0200 @@ -303,6 +303,11 @@ return str_; } + bool IsNullOrEmpty() const + { + return str_ == NULL || str_[0] == 0; + } + void ToString(std::string& target) const; void ToJson(Json::Value& target) const; @@ -610,6 +615,10 @@ unsigned int minor, unsigned int revision); + bool CheckMinimalVersion(const char* version, + unsigned int major, + unsigned int minor, + unsigned int revision); namespace Internals { diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -0,0 +1,679 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2022 Osimis S.A., Belgium + * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 "../../../../OrthancFramework/Sources/Compatibility.h" +#include "../Common/OrthancPluginCppWrapper.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int globalPropertyId_ = 0; +static bool force_ = false; +static uint throttleDelay_ = 0; +static std::unique_ptr workerThread_; +static bool workerThreadShouldStop_ = false; +static bool triggerOnStorageCompressionChange_ = true; +static bool triggerOnMainDicomTagsChange_ = true; +static bool triggerOnUnnecessaryDicomAsJsonFiles_ = true; + + +struct RunningPeriod +{ + int fromHour_; + int toHour_; + int weekday_; + + RunningPeriod(const std::string& weekday, const std::string& period) + { + if (weekday == "Monday") + { + weekday_ = 1; + } + else if (weekday == "Tuesday") + { + weekday_ = 2; + } + else if (weekday == "Wednesday") + { + weekday_ = 3; + } + else if (weekday == "Thursday") + { + weekday_ = 4; + } + else if (weekday == "Friday") + { + weekday_ = 5; + } + else if (weekday == "Saturday") + { + weekday_ = 6; + } + else if (weekday == "Sunday") + { + weekday_ = 0; + } + else + { + OrthancPlugins::LogWarning("Housekeeper: invalid schedule: unknown 'day': " + weekday); + ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat); + } + + std::vector hours; + boost::split(hours, period, boost::is_any_of("-")); + + fromHour_ = boost::lexical_cast(hours[0]); + toHour_ = boost::lexical_cast(hours[1]); + } + + bool isInPeriod() const + { + time_t now = time(NULL); + tm* nowLocalTime = localtime(&now); + + if (nowLocalTime->tm_wday != weekday_) + { + return false; + } + + if (nowLocalTime->tm_hour >= fromHour_ && nowLocalTime->tm_hour < toHour_) + { + return true; + } + + return false; + } +}; + +struct RunningPeriods +{ + std::list runningPeriods_; + + void load(const Json::Value& scheduleConfiguration) + { +// "Monday": ["0-6", "20-24"], + + Json::Value::Members names = scheduleConfiguration.getMemberNames(); + + for (Json::Value::Members::const_iterator it = names.begin(); + it != names.end(); it++) + { + for (Json::Value::ArrayIndex i = 0; i < scheduleConfiguration[*it].size(); i++) + { + runningPeriods_.push_back(RunningPeriod(*it, scheduleConfiguration[*it][i].asString())); + } + } + } + + bool isInPeriod() + { + if (runningPeriods_.size() == 0) + { + return true; // if no config: always run + } + + for (std::list::const_iterator it = runningPeriods_.begin(); + it != runningPeriods_.end(); it++) + { + if (it->isInPeriod()) + { + return true; + } + } + return false; + } +}; + +RunningPeriods runningPeriods_; + +struct DbConfiguration +{ + std::string orthancVersion; + std::map mainDicomTagsSignature; + bool storageCompressionEnabled; + + DbConfiguration() + : storageCompressionEnabled(false) + { + } + + bool IsDefined() const + { + return !orthancVersion.empty() && mainDicomTagsSignature.size() == 4; + } + + void Clear() + { + orthancVersion.clear(); + mainDicomTagsSignature.clear(); + } + + void ToJson(Json::Value& target) + { + if (!IsDefined()) + { + target = Json::nullValue; + } + else + { + Json::Value signatures; + + target = Json::objectValue; + + // default main dicom tags signature are the one from Orthanc 1.4.2 (last time the list was changed): + signatures["Patient"] = mainDicomTagsSignature[OrthancPluginResourceType_Patient]; + signatures["Study"] = mainDicomTagsSignature[OrthancPluginResourceType_Study]; + signatures["Series"] = mainDicomTagsSignature[OrthancPluginResourceType_Series]; + signatures["Instance"] = mainDicomTagsSignature[OrthancPluginResourceType_Instance]; + + target["MainDicomTagsSignature"] = signatures; + target["OrthancVersion"] = orthancVersion; + target["StorageCompressionEnabled"] = storageCompressionEnabled; + } + } + + void FromJson(Json::Value& source) + { + if (!source.isNull()) + { + orthancVersion = source["OrthancVersion"].asString(); + + const Json::Value& signatures = source["MainDicomTagsSignature"]; + mainDicomTagsSignature[OrthancPluginResourceType_Patient] = signatures["Patient"].asString(); + mainDicomTagsSignature[OrthancPluginResourceType_Study] = signatures["Study"].asString(); + mainDicomTagsSignature[OrthancPluginResourceType_Series] = signatures["Series"].asString(); + mainDicomTagsSignature[OrthancPluginResourceType_Instance] = signatures["Instance"].asString(); + + storageCompressionEnabled = source["StorageCompressionEnabled"].asBool(); + } + } +}; + +struct PluginStatus +{ + int statusVersion; + int64_t lastProcessedChange; + int64_t lastChangeToProcess; + + DbConfiguration currentlyProcessingConfiguration; // last configuration being processed (has not reached last change yet) + DbConfiguration lastProcessedConfiguration; // last configuration that has been fully processed (till last change) + + PluginStatus() + : statusVersion(1), + lastProcessedChange(-1), + lastChangeToProcess(-1) + { + } + + void ToJson(Json::Value& target) + { + target = Json::objectValue; + + target["Version"] = statusVersion; + target["LastProcessedChange"] = Json::Value::Int64(lastProcessedChange); + target["LastChangeToProcess"] = Json::Value::Int64(lastChangeToProcess); + + currentlyProcessingConfiguration.ToJson(target["CurrentlyProcessingConfiguration"]); + lastProcessedConfiguration.ToJson(target["LastProcessedConfiguration"]); + } + + void FromJson(Json::Value& source) + { + statusVersion = source["Version"].asInt(); + lastProcessedChange = source["LastProcessedChange"].asInt64(); + lastChangeToProcess = source["LastChangeToProcess"].asInt64(); + + Json::Value& current = source["CurrentlyProcessingConfiguration"]; + Json::Value& last = source["LastProcessedConfiguration"]; + + currentlyProcessingConfiguration.FromJson(current); + lastProcessedConfiguration.FromJson(last); + } +}; + + +static void ReadStatusFromDb(PluginStatus& pluginStatus) +{ + OrthancPlugins::OrthancString globalPropertyContent; + + globalPropertyContent.Assign(OrthancPluginGetGlobalProperty(OrthancPlugins::GetGlobalContext(), + globalPropertyId_, + "")); + + if (!globalPropertyContent.IsNullOrEmpty()) + { + Json::Value jsonStatus; + globalPropertyContent.ToJson(jsonStatus); + pluginStatus.FromJson(jsonStatus); + } + else + { + // default config + pluginStatus.statusVersion = 1; + pluginStatus.lastProcessedChange = -1; + pluginStatus.lastChangeToProcess = -1; + + pluginStatus.currentlyProcessingConfiguration.orthancVersion = "1.9.0"; // when we don't know, we assume some files were stored with Orthanc 1.9.0 (last version saving the dicom-as-json files) + + // default main dicom tags signature are the one from Orthanc 1.4.2 (last time the list was changed): + pluginStatus.currentlyProcessingConfiguration.mainDicomTagsSignature[OrthancPluginResourceType_Patient] = "0010,0010;0010,0020;0010,0030;0010,0040;0010,1000"; + pluginStatus.currentlyProcessingConfiguration.mainDicomTagsSignature[OrthancPluginResourceType_Study] = "0008,0020;0008,0030;0008,0050;0008,0080;0008,0090;0008,1030;0020,000d;0020,0010;0032,1032;0032,1060"; + pluginStatus.currentlyProcessingConfiguration.mainDicomTagsSignature[OrthancPluginResourceType_Series] = "0008,0021;0008,0031;0008,0060;0008,0070;0008,1010;0008,103e;0008,1070;0018,0010;0018,0015;0018,0024;0018,1030;0018,1090;0018,1400;0020,000e;0020,0011;0020,0037;0020,0105;0020,1002;0040,0254;0054,0081;0054,0101;0054,1000"; + pluginStatus.currentlyProcessingConfiguration.mainDicomTagsSignature[OrthancPluginResourceType_Instance] = "0008,0012;0008,0013;0008,0018;0020,0012;0020,0013;0020,0032;0020,0037;0020,0100;0020,4000;0028,0008;0054,1330"; + } +} + +static void SaveStatusInDb(PluginStatus& pluginStatus) +{ + Json::Value jsonStatus; + pluginStatus.ToJson(jsonStatus); + + Json::StreamWriterBuilder builder; + builder.settings_["indentation"] = " "; + std::string serializedStatus = Json::writeString(builder, jsonStatus); + + OrthancPluginSetGlobalProperty(OrthancPlugins::GetGlobalContext(), + globalPropertyId_, + serializedStatus.c_str()); +} + +static void GetCurrentDbConfiguration(DbConfiguration& configuration) +{ + Json::Value signatures; + Json::Value systemInfo; + + OrthancPlugins::RestApiGet(systemInfo, "/system", false); + configuration.mainDicomTagsSignature[OrthancPluginResourceType_Patient] = systemInfo["MainDicomTags"]["Patient"].asString(); + configuration.mainDicomTagsSignature[OrthancPluginResourceType_Study] = systemInfo["MainDicomTags"]["Study"].asString(); + configuration.mainDicomTagsSignature[OrthancPluginResourceType_Series] = systemInfo["MainDicomTags"]["Series"].asString(); + configuration.mainDicomTagsSignature[OrthancPluginResourceType_Instance] = systemInfo["MainDicomTags"]["Instance"].asString(); + configuration.storageCompressionEnabled = systemInfo["StorageCompression"].asBool(); + + configuration.orthancVersion = OrthancPlugins::GetGlobalContext()->orthancVersion; +} + +static bool NeedsProcessing(const DbConfiguration& current, const DbConfiguration& last) +{ + if (!last.IsDefined()) + { + return true; + } + + const char* lastVersion = last.orthancVersion.c_str(); + const std::map& lastTags = last.mainDicomTagsSignature; + const std::map& currentTags = current.mainDicomTagsSignature; + bool needsProcessing = false; + + if (!OrthancPlugins::CheckMinimalVersion(lastVersion, 1, 9, 1)) + { + if (triggerOnUnnecessaryDicomAsJsonFiles_) + { + OrthancPlugins::LogWarning("Housekeeper: your storage might still contain some dicom-as-json files -> will perform housekeeping"); + needsProcessing = true; + } + else + { + OrthancPlugins::LogWarning("Housekeeper: your storage might still contain some dicom-as-json files but the trigger has been disabled"); + } + } + + if (lastTags.at(OrthancPluginResourceType_Patient) != currentTags.at(OrthancPluginResourceType_Patient)) + { + if (triggerOnMainDicomTagsChange_) + { + OrthancPlugins::LogWarning("Housekeeper: Patient main dicom tags have changed, -> will perform housekeeping"); + needsProcessing = true; + } + else + { + OrthancPlugins::LogWarning("Housekeeper: Patient main dicom tags have changed but the trigger is disabled"); + } + } + + if (lastTags.at(OrthancPluginResourceType_Study) != currentTags.at(OrthancPluginResourceType_Study)) + { + if (triggerOnMainDicomTagsChange_) + { + OrthancPlugins::LogWarning("Housekeeper: Study main dicom tags have changed, -> will perform housekeeping"); + needsProcessing = true; + } + else + { + OrthancPlugins::LogWarning("Housekeeper: Study main dicom tags have changed but the trigger is disabled"); + } + } + + if (lastTags.at(OrthancPluginResourceType_Series) != currentTags.at(OrthancPluginResourceType_Series)) + { + if (triggerOnMainDicomTagsChange_) + { + OrthancPlugins::LogWarning("Housekeeper: Series main dicom tags have changed, -> will perform housekeeping"); + needsProcessing = true; + } + else + { + OrthancPlugins::LogWarning("Housekeeper: Series main dicom tags have changed but the trigger is disabled"); + } + } + + if (lastTags.at(OrthancPluginResourceType_Instance) != currentTags.at(OrthancPluginResourceType_Instance)) + { + if (triggerOnMainDicomTagsChange_) + { + OrthancPlugins::LogWarning("Housekeeper: Instance main dicom tags have changed, -> will perform housekeeping"); + needsProcessing = true; + } + else + { + OrthancPlugins::LogWarning("Housekeeper: Instance main dicom tags have changed but the trigger is disabled"); + } + } + + if (current.storageCompressionEnabled != last.storageCompressionEnabled) + { + if (triggerOnStorageCompressionChange_) + { + if (current.storageCompressionEnabled) + { + OrthancPlugins::LogWarning("Housekeeper: storage compression is now enabled -> will perform housekeeping"); + } + else + { + OrthancPlugins::LogWarning("Housekeeper: storage compression is now disabled -> will perform housekeeping"); + } + + needsProcessing = true; + } + else + { + OrthancPlugins::LogWarning("Housekeeper: storage compression has changed but the trigger is disabled"); + } + } + + return needsProcessing; +} + +static bool ProcessChanges(PluginStatus& pluginStatus, const DbConfiguration& currentDbConfiguration) +{ + Json::Value changes; + + pluginStatus.currentlyProcessingConfiguration = currentDbConfiguration; + + OrthancPlugins::RestApiGet(changes, "/changes?since=" + boost::lexical_cast(pluginStatus.lastProcessedChange) + "&limit=100", false); + + for (Json::ArrayIndex i = 0; i < changes["Changes"].size(); i++) + { + const Json::Value& change = changes["Changes"][i]; + int64_t seq = change["Seq"].asInt64(); + + if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed + { + Json::Value result; + OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", std::string(""), false); + boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 1000)); + } + + if (seq >= pluginStatus.lastChangeToProcess) // we are done ! + { + return true; + } + + pluginStatus.lastProcessedChange = seq; + } + + return false; +} + + +static void WorkerThread() +{ + PluginStatus pluginStatus; + DbConfiguration currentDbConfiguration; + + OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Starting Housekeeper worker thread"); + + ReadStatusFromDb(pluginStatus); + GetCurrentDbConfiguration(currentDbConfiguration); + + if (!NeedsProcessing(currentDbConfiguration, pluginStatus.lastProcessedConfiguration)) + { + OrthancPlugins::LogWarning("Housekeeper: everything has been processed already !"); + return; + } + + if (force_ || NeedsProcessing(currentDbConfiguration, pluginStatus.currentlyProcessingConfiguration)) + { + if (force_) + { + OrthancPlugins::LogWarning("Housekeeper: forcing execution -> will perform housekeeping"); + } + else + { + OrthancPlugins::LogWarning("Housekeeper: the DB configuration has changed since last run, will reprocess the whole DB !"); + } + + Json::Value changes; + OrthancPlugins::RestApiGet(changes, "/changes?last", false); + + pluginStatus.lastProcessedChange = 0; + pluginStatus.lastChangeToProcess = changes["Last"].asInt64(); // the last change is the last change at the time we start. We assume that every new ingested file will be constructed correctly + } + else + { + OrthancPlugins::LogWarning("Housekeeper: the DB configuration has not changed since last run, will continue processing changes"); + } + + bool completed = pluginStatus.lastChangeToProcess == 0; // if the DB is empty at start, no need to process anyting + bool loggedNotRightPeriodChangeMessage = false; + + while (!workerThreadShouldStop_ && !completed) + { + if (runningPeriods_.isInPeriod()) + { + completed = ProcessChanges(pluginStatus, currentDbConfiguration); + SaveStatusInDb(pluginStatus); + + if (!completed) + { + OrthancPlugins::LogInfo("Housekeeper: processed changes " + + boost::lexical_cast(pluginStatus.lastProcessedChange) + + " / " + boost::lexical_cast(pluginStatus.lastChangeToProcess)); + + boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 100)); // wait 1/10 of the delay between changes + } + + loggedNotRightPeriodChangeMessage = false; + } + else + { + if (!loggedNotRightPeriodChangeMessage) + { + OrthancPlugins::LogInfo("Housekeeper: entering quiet period"); + loggedNotRightPeriodChangeMessage = true; + } + } + } + + if (completed) + { + pluginStatus.lastProcessedConfiguration = currentDbConfiguration; + pluginStatus.currentlyProcessingConfiguration.Clear(); + + pluginStatus.lastProcessedChange = -1; + pluginStatus.lastChangeToProcess = -1; + + SaveStatusInDb(pluginStatus); + + OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Housekeeper: finished processing all changes"); + } +} + +extern "C" +{ + OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char* resourceId) + { + switch (changeType) + { + case OrthancPluginChangeType_OrthancStarted: + { + workerThread_.reset(new boost::thread(WorkerThread)); + return OrthancPluginErrorCode_Success; + } + case OrthancPluginChangeType_OrthancStopped: + { + if (workerThread_ && workerThread_->joinable()) + { + workerThreadShouldStop_ = true; + workerThread_->join(); + } + } + default: + return OrthancPluginErrorCode_Success; + } + } + + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + OrthancPlugins::SetGlobalContext(c); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 0) + { + OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, + ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); + return -1; + } + + OrthancPlugins::LogWarning("Housekeeper plugin is initializing"); + OrthancPluginSetDescription(c, "Optimizes your DB and storage."); + + OrthancPlugins::OrthancConfiguration configuration; + + OrthancPlugins::OrthancConfiguration housekeeper; + configuration.GetSection(housekeeper, "Housekeeper"); + + bool enabled = housekeeper.GetBooleanValue("Enable", false); + if (enabled) + { + /* + { + "Housekeeper": { + + // Enables/disables the plugin + "Enable": false, + + // the Global Prooperty ID in which the plugin progress + // is stored. Must be > 1024 and must not be used by + // another plugin + "GlobalPropertyId": 1025, + + // Forces execution even if the plugin did not detect + // any changes in configuration + "Force": false, + + // Delay (in seconds) between reconstruction of 2 studies + // This avoids overloading Orthanc with the housekeeping + // process and leaves room for other operations. + "ThrottleDelay": 5, + + // Runs the plugin only at certain period of time. + // If not specified, the plugin runs all the time + // Examples: + // to run between 0AM and 6AM everyday + every night + // from 8PM to 12PM and 24h a day on the weekend: + // "Schedule": { + // "Monday": ["0-6", "20-24"], + // "Tuesday": ["0-6", "20-24"], + // "Wednesday": ["0-6", "20-24"], + // "Thursday": ["0-6", "20-24"], + // "Friday": ["0-6", "20-24"], + // "Saturday": ["0-24"], + // "Sunday": ["0-24"] + // }, + + // configure events that can trigger a housekeeping processing + "Triggers" : { + "StorageCompressionChange": true, + "MainDicomTagsChange": true, + "UnnecessaryDicomAsJsonFiles": true + } + + } + } + */ + + + globalPropertyId_ = housekeeper.GetIntegerValue("GlobalPropertyId", 1025); + force_ = housekeeper.GetBooleanValue("Force", false); + throttleDelay_ = housekeeper.GetUnsignedIntegerValue("ThrottleDelay", 5); + + if (housekeeper.GetJson().isMember("Triggers")) + { + triggerOnStorageCompressionChange_ = housekeeper.GetBooleanValue("StorageCompressionChange", true); + triggerOnMainDicomTagsChange_ = housekeeper.GetBooleanValue("MainDicomTagsChange", true); + triggerOnUnnecessaryDicomAsJsonFiles_ = housekeeper.GetBooleanValue("UnnecessaryDicomAsJsonFiles", true); + } + + if (housekeeper.GetJson().isMember("Schedule")) + { + runningPeriods_.load(housekeeper.GetJson()["Schedule"]); + } + + OrthancPluginRegisterOnChangeCallback(c, OnChangeCallback); + } + else + { + OrthancPlugins::LogWarning("Housekeeper plugin is disabled by the configuration file"); + } + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + OrthancPlugins::LogWarning("Housekeeper plugin is finalizing"); + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "housekeeper"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return DB_OPTIMIZER_VERSION; + } +} diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Resources/Configuration.json --- a/OrthancServer/Resources/Configuration.json Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Resources/Configuration.json Mon Apr 25 15:50:57 2022 +0200 @@ -858,5 +858,49 @@ // (default behaviour). A value > 1 is meaningful only if the storage // is a distributed network storage (e.g object storage plugin). // (new experimental feature in Orthanc 1.10.0) - "ZipLoaderThreads": 0 + "ZipLoaderThreads": 0, + + // Extra Main Dicom tags that are stored in DB together with all default + // Main Dicom tags that are already stored (TODO: see book new page). + // (new in Orthanc 1.11.0) + // Sequences tags are not supported. + /** + "ExtraMainDicomTags" : { + "Instance" : [ + "Rows", + "Columns", + "ImageType", + "SOPClassUID", + "ContentDate", + "ContentTime", + "FrameOfReferenceUID", + "PixelSpacing", + "SpecificCharacterSet", + "BitsAllocated" + ], + "Series" : [], + "Study": [], + "Patient": [] + }, + */ + + // Enables/disables warnings in the logs. + // "true" enables a warning. All warnings are enabled by default + // TODO: see book new page + // (new in Orthanc 1.11.0) + "Warnings" : { + // A "RequestedTags" has been read from storage which is slower than + // reading it from DB. + // You might want to store this tag in ExtraMainDicomTags to build + // the response faster. + "W001_TagsBeingReadFromStorage": true, + + // Retrieving a list of Main dicom tags from a resource that has been + // saved with another "ExtraMainDicomTags" configuration which means that + // your response might be incomplete/inconsistent. + // You should call patients|studies|series|instances/../reconstruct to rebuild + // the DB. TODO: also check for "rebuild DB" plugin + "W002_InconsistentDicomTagsInDb": true + } + } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -362,8 +362,7 @@ } { - std::set tags; - DicomMap::GetMainDicomTags(tags, level); + const std::set& tags = DicomMap::GetMainDicomTags(level); for (std::set::const_iterator tag = tags.begin(); tag != tags.end(); ++tag) @@ -711,46 +710,16 @@ } - bool StatelessDatabaseOperations::ExpandResource(Json::Value& target, + bool StatelessDatabaseOperations::ExpandResource(ExpandedResource& target, const std::string& publicId, ResourceType level, - DicomToJsonFormat format) + const std::set& requestedTags, + ExpandResourceDbFlags expandFlags) { - class Operations : public ReadOnlyOperationsT5< - bool&, Json::Value&, const std::string&, ResourceType, DicomToJsonFormat> + class Operations : public ReadOnlyOperationsT6< + bool&, ExpandedResource&, const std::string&, ResourceType, const std::set&, ExpandResourceDbFlags> { private: - static void MainDicomTagsToJson(ReadOnlyTransaction& transaction, - Json::Value& target, - int64_t resourceId, - ResourceType resourceType, - DicomToJsonFormat format) - { - static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; - static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags"; - - DicomMap tags; - transaction.GetMainDicomTags(tags, resourceId); - - if (resourceType == ResourceType_Study) - { - DicomMap t1, t2; - tags.ExtractStudyInformation(t1); - tags.ExtractPatientInformation(t2); - - target[MAIN_DICOM_TAGS] = Json::objectValue; - FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], t1, format); - - target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue; - FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], t2, format); - } - else - { - target[MAIN_DICOM_TAGS] = Json::objectValue; - FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], tags, format); - } - } - static bool LookupStringMetadata(std::string& result, const std::map& metadata, @@ -797,7 +766,7 @@ const Tuple& tuple) ORTHANC_OVERRIDE { // Lookup for the requested resource - int64_t internalId; // unused + int64_t internalId; ResourceType type; std::string parent; if (!transaction.LookupResourceAndParent(internalId, type, parent, tuple.get<2>()) || @@ -807,9 +776,9 @@ } else { - Json::Value& target = tuple.get<1>(); - target = Json::objectValue; - + ExpandedResource& target = tuple.get<1>(); + ExpandResourceDbFlags expandFlags = tuple.get<5>(); + // Set information about the parent resource (if it exists) if (type == ResourceType_Patient) { @@ -825,150 +794,166 @@ throw OrthancException(ErrorCode_DatabasePlugin); } - switch (type) - { - case ResourceType_Study: - target["ParentPatient"] = parent; - break; - - case ResourceType_Series: - target["ParentStudy"] = parent; - break; - - case ResourceType_Instance: - target["ParentSeries"] = parent; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } + target.parentId_ = parent; } - // List the children resources - std::list children; - transaction.GetChildrenPublicId(children, internalId); - - if (type != ResourceType_Instance) + target.type_ = type; + target.id_ = tuple.get<2>(); + + if (expandFlags & ExpandResourceDbFlags_IncludeChildren) { - Json::Value c = Json::arrayValue; - - for (std::list::const_iterator - it = children.begin(); it != children.end(); ++it) - { - c.append(*it); - } + // List the children resources + transaction.GetChildrenPublicId(target.childrenIds_, internalId); + } + + if (expandFlags & ExpandResourceDbFlags_IncludeMetadata) + { + // Extract the metadata + transaction.GetAllMetadata(target.metadata_, internalId); switch (type) { case ResourceType_Patient: - target["Studies"] = c; - break; - case ResourceType_Study: - target["Series"] = c; break; case ResourceType_Series: - target["Instances"] = c; + { + int64_t i; + if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Series_ExpectedNumberOfInstances)) + { + target.expectedNumberOfInstances_ = static_cast(i); + target.status_ = EnumerationToString(transaction.GetSeriesStatus(internalId, i)); + } + else + { + target.expectedNumberOfInstances_ = -1; + target.status_ = EnumerationToString(SeriesStatus_Unknown); + } + break; + } + + case ResourceType_Instance: + { + FileInfo attachment; + int64_t revision; // ignored + if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom)) + { + throw OrthancException(ErrorCode_InternalError); + } + + target.fileSize_ = static_cast(attachment.GetUncompressedSize()); + target.fileUuid_ = attachment.GetUuid(); + + int64_t i; + if (LookupIntegerMetadata(i, target.metadata_, MetadataType_Instance_IndexInSeries)) + { + target.indexInSeries_ = static_cast(i); + } + else + { + target.indexInSeries_ = -1; + } + + break; + } default: throw OrthancException(ErrorCode_InternalError); } + + // check the main dicom tags list has not changed since the resource was stored + target.mainDicomTagsSignature_ = DicomMap::GetDefaultMainDicomTagsSignature(type); + LookupStringMetadata(target.mainDicomTagsSignature_, target.metadata_, MetadataType_MainDicomTagsSignature); } - // Extract the metadata - std::map metadata; - transaction.GetAllMetadata(metadata, internalId); - - // Set the resource type - switch (type) + if (expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags) { - case ResourceType_Patient: - target["Type"] = "Patient"; - break; - - case ResourceType_Study: - target["Type"] = "Study"; - break; - - case ResourceType_Series: + // read all tags from DB + transaction.GetMainDicomTags(target.tags_, internalId); + + // check if we have access to all requestedTags or if we must get tags from parents + const std::set& requestedTags = tuple.get<4>(); + + if (requestedTags.size() > 0) { - target["Type"] = "Series"; - - int64_t i; - if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances)) + std::set savedMainDicomTags; + + FromDcmtkBridge::ParseListOfTags(savedMainDicomTags, target.mainDicomTagsSignature_); + + // read parent main dicom tags as long as we don't have gathered all requested tags + ResourceType currentLevel = target.type_; + int64_t currentInternalId = internalId; + Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags); + + while ((target.missingRequestedTags_.size() > 0) + && currentLevel != ResourceType_Patient) { - target["ExpectedNumberOfInstances"] = static_cast(i); - target["Status"] = EnumerationToString(transaction.GetSeriesStatus(internalId, i)); + currentLevel = GetParentResourceType(currentLevel); + + int64_t currentParentId; + if (!transaction.LookupParent(currentParentId, currentInternalId)) + { + break; + } + + std::map parentMetadata; + transaction.GetAllMetadata(parentMetadata, currentParentId); + + std::string parentMainDicomTagsSignature = DicomMap::GetDefaultMainDicomTagsSignature(currentLevel); + LookupStringMetadata(parentMainDicomTagsSignature, parentMetadata, MetadataType_MainDicomTagsSignature); + + std::set parentSavedMainDicomTags; + FromDcmtkBridge::ParseListOfTags(parentSavedMainDicomTags, parentMainDicomTagsSignature); + + size_t previousMissingCount = target.missingRequestedTags_.size(); + Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags); + Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags); + + // read the parent tags from DB only if it reduces the number of missing tags + if (target.missingRequestedTags_.size() < previousMissingCount) + { + Toolbox::AppendSets(savedMainDicomTags, parentSavedMainDicomTags); + + DicomMap parentTags; + transaction.GetMainDicomTags(parentTags, currentParentId); + + target.tags_.Merge(parentTags); + } + + currentInternalId = currentParentId; } - else - { - target["ExpectedNumberOfInstances"] = Json::nullValue; - target["Status"] = EnumerationToString(SeriesStatus_Unknown); - } - - break; } - - case ResourceType_Instance: - { - target["Type"] = "Instance"; - - FileInfo attachment; - int64_t revision; // ignored - if (!transaction.LookupAttachment(attachment, revision, internalId, FileContentType_Dicom)) - { - throw OrthancException(ErrorCode_InternalError); - } - - target["FileSize"] = static_cast(attachment.GetUncompressedSize()); - target["FileUuid"] = attachment.GetUuid(); - - int64_t i; - if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries)) - { - target["IndexInSeries"] = static_cast(i); - } - else - { - target["IndexInSeries"] = Json::nullValue; - } - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); } - // Record the remaining information - target["ID"] = tuple.get<2>(); - MainDicomTagsToJson(transaction, target, internalId, type, tuple.get<4>()); - std::string tmp; - if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom)) + if (LookupStringMetadata(tmp, target.metadata_, MetadataType_AnonymizedFrom)) { - target["AnonymizedFrom"] = tmp; + target.anonymizedFrom_ = tmp; } - if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom)) + if (LookupStringMetadata(tmp, target.metadata_, MetadataType_ModifiedFrom)) { - target["ModifiedFrom"] = tmp; + target.modifiedFrom_ = tmp; } if (type == ResourceType_Patient || type == ResourceType_Study || type == ResourceType_Series) { - target["IsStable"] = !transaction.GetTransactionContext().IsUnstableResource(internalId); - - if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate)) + target.isStable_ = !transaction.GetTransactionContext().IsUnstableResource(internalId); + + if (LookupStringMetadata(tmp, target.metadata_, MetadataType_LastUpdate)) { - target["LastUpdate"] = tmp; + target.lastUpdate_ = tmp; } } + else + { + target.isStable_ = false; + } tuple.get<0>() = true; } @@ -977,7 +962,7 @@ bool found; Operations operations; - operations.Apply(*this, found, target, publicId, level, format); + operations.Apply(*this, found, target, publicId, level, requestedTags, expandFlags); return found; } @@ -2701,7 +2686,13 @@ content.AddResource(study, ResourceType_Study, summary_); content.AddResource(series, ResourceType_Series, summary_); content.AddResource(instance, ResourceType_Instance, summary_); + transaction.SetResourcesContent(content); + + ReplaceMetadata(transaction, patient, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient)); // New in Orthanc 1.11.0 + ReplaceMetadata(transaction, study, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study)); // New in Orthanc 1.11.0 + ReplaceMetadata(transaction, series, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series)); // New in Orthanc 1.11.0 + ReplaceMetadata(transaction, instance, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance)); // New in Orthanc 1.11.0 } if (hasTransferSyntax_) @@ -2716,6 +2707,7 @@ { ReplaceMetadata(transaction, instance, MetadataType_Instance_SopClassUid, value->GetContent()); } + } }; @@ -3082,20 +3074,24 @@ // Populate the tags of the newly-created resources content.AddResource(instanceId, ResourceType_Instance, dicomSummary_); + content.AddMetadata(instanceId, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance)); // New in Orthanc 1.11.0 if (status.isNewSeries_) { content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary_); + content.AddMetadata(status.seriesId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series)); // New in Orthanc 1.11.0 } if (status.isNewStudy_) { content.AddResource(status.studyId_, ResourceType_Study, dicomSummary_); + content.AddMetadata(status.studyId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study)); // New in Orthanc 1.11.0 } if (status.isNewPatient_) { content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary_); + content.AddMetadata(status.patientId_, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient)); // New in Orthanc 1.11.0 } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/Database/StatelessDatabaseOperations.h --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Mon Apr 25 15:50:57 2022 +0200 @@ -37,6 +37,45 @@ class ParsedDicomFile; struct ServerIndexChange; + struct ExpandedResource : public boost::noncopyable + { + std::string id_; + DicomMap tags_; // all tags from DB + std::string mainDicomTagsSignature_; + std::string parentId_; + std::list childrenIds_; + std::map metadata_; + ResourceType type_; + std::string anonymizedFrom_; + std::string modifiedFrom_; + std::string lastUpdate_; + std::set missingRequestedTags_; + + // for patients/studies/series + bool isStable_; + + // for series only + int expectedNumberOfInstances_; + std::string status_; + + // for instances only + size_t fileSize_; + std::string fileUuid_; + int indexInSeries_; + }; + + enum ExpandResourceDbFlags + { + ExpandResourceDbFlags_None = 0, + ExpandResourceDbFlags_IncludeMetadata = (1 << 0), + ExpandResourceDbFlags_IncludeChildren = (1 << 1), + ExpandResourceDbFlags_IncludeMainDicomTags = (1 << 2), + + ExpandResourceDbFlags_Default = (ExpandResourceDbFlags_IncludeMetadata | + ExpandResourceDbFlags_IncludeChildren | + ExpandResourceDbFlags_IncludeMainDicomTags) + }; + class StatelessDatabaseOperations : public boost::noncopyable { public: @@ -448,10 +487,11 @@ void Apply(IReadWriteOperations& operations); - bool ExpandResource(Json::Value& target, + bool ExpandResource(ExpandedResource& target, const std::string& publicId, ResourceType level, - DicomToJsonFormat format); + const std::set& requestedTags, + ExpandResourceDbFlags expandFlags); void GetAllMetadata(std::map& target, const std::string& publicId, diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/OrthancConfiguration.cpp --- a/OrthancServer/Sources/OrthancConfiguration.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -43,6 +43,7 @@ static const char* const ORTHANC_PEERS_IN_DB = "OrthancPeersInDatabase"; static const char* const TEMPORARY_DIRECTORY = "TemporaryDirectory"; static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier"; +static const char* const WARNINGS = "Warnings"; namespace Orthanc { @@ -1055,7 +1056,51 @@ } } - + void OrthancConfiguration::LoadWarnings() + { + if (json_.isMember(WARNINGS)) + { + const Json::Value& warnings = json_[WARNINGS]; + if (!warnings.isObject()) + { + throw OrthancException(ErrorCode_BadFileFormat, std::string(WARNINGS) + " configuration entry is not a Json object"); + } + + Json::Value::Members members = warnings.getMemberNames(); + + for (size_t i = 0; i < members.size(); i++) + { + const std::string& name = members[i]; + bool enabled = warnings[name].asBool(); + + Warnings warning = Warnings_None; + if (name == "W001_TagsBeingReadFromStorage") + { + warning = Warnings_001_TagsBeingReadFromStorage; + } + else if (name == "W002_InconsistentDicomTagsInDb") + { + warning = Warnings_002_InconsistentDicomTagsInDb; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, name + " is not recognized as a valid warning name"); + } + + if (!enabled) + { + disabledWarnings_.insert(warning); + } + } + } + else + { + disabledWarnings_.clear(); + } + + } + + void OrthancConfiguration::DefaultExtractDicomSummary(DicomMap& target, const ParsedDicomFile& dicom) { diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/OrthancConfiguration.h --- a/OrthancServer/Sources/OrthancConfiguration.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.h Mon Apr 25 15:50:57 2022 +0200 @@ -27,10 +27,12 @@ #include "../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h" #include +#include "ServerEnumerations.h" #include #include #include +#include class DcmDataset; @@ -42,7 +44,8 @@ class ParsedDicomFile; class ServerIndex; class TemporaryFile; - + + class OrthancConfiguration : public boost::noncopyable { private: @@ -58,6 +61,7 @@ Modalities modalities_; Peers peers_; ServerIndex* serverIndex_; + std::set disabledWarnings_; OrthancConfiguration() : configurationFileArg_(NULL), @@ -153,7 +157,9 @@ // "SetServerIndex()" must have been called void LoadModalitiesAndPeers(); - + + void LoadWarnings(); + void RegisterFont(ServerResources::FileResourceId resource); bool LookupStringParameter(std::string& target, @@ -242,6 +248,11 @@ std::string GetDatabaseServerIdentifier() const; + bool IsWarningEnabled(Warnings warning) const + { + return disabledWarnings_.count(warning) == 0; + } + static void DefaultExtractDicomSummary(DicomMap& target, const ParsedDicomFile& dicom); diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/OrthancFindRequestHandler.cpp --- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -38,275 +38,28 @@ namespace Orthanc { - static void GetChildren(std::list& target, - ServerIndex& index, - const std::list& source) - { - target.clear(); - - for (std::list::const_iterator - it = source.begin(); it != source.end(); ++it) - { - std::list tmp; - index.GetChildren(tmp, *it); - target.splice(target.end(), tmp); - } - } - - - static void StoreSetOfStrings(DicomMap& result, - const DicomTag& tag, - const std::set& values) - { - bool isFirst = true; - - std::string s; - for (std::set::const_iterator - it = values.begin(); it != values.end(); ++it) - { - if (isFirst) - { - isFirst = false; - } - else - { - s += "\\"; - } - - s += *it; - } - - result.SetValue(tag, s, false); - } - - - static void ComputePatientCounters(DicomMap& result, - ServerIndex& index, - const std::string& patient, - const DicomMap& query) - { - std::list studies; - index.GetChildren(studies, patient); - - if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES)) - { - result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, - boost::lexical_cast(studies.size()), false); - } - - if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) && - !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) - { - return; - } - - std::list series; - GetChildren(series, index, studies); - studies.clear(); // This information is useless below - - if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES)) - { - result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, - boost::lexical_cast(series.size()), false); - } - - if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) - { - return; - } - - std::list instances; - GetChildren(instances, index, series); - - if (query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) - { - result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, - boost::lexical_cast(instances.size()), false); - } - } - - - static void ComputeStudyCounters(DicomMap& result, - ServerContext& context, - const std::string& study, - const DicomMap& query) - { - ServerIndex& index = context.GetIndex(); - - std::list series; - index.GetChildren(series, study); - - if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES)) - { - result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, - boost::lexical_cast(series.size()), false); - } - - if (query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) - { - std::set values; - - for (std::list::const_iterator - it = series.begin(); it != series.end(); ++it) - { - DicomMap tags; - if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series)) - { - const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); - - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) - { - values.insert(value->GetContent()); - } - } - } - - StoreSetOfStrings(result, DICOM_TAG_MODALITIES_IN_STUDY, values); - } - - if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) && - !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY)) - { - return; - } - - std::list instances; - GetChildren(instances, index, series); - - if (query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES)) - { - result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, - boost::lexical_cast(instances.size()), false); - } - - if (query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY)) - { - std::set values; - - for (std::list::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - std::string value; - if (context.LookupOrReconstructMetadata(value, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid)) - { - values.insert(value); - } - } - - StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values); - } - } - - - static void ComputeSeriesCounters(DicomMap& result, - ServerIndex& index, - const std::string& series, - const DicomMap& query) - { - std::list instances; - index.GetChildren(instances, series); - - if (query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES)) - { - result.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, - boost::lexical_cast(instances.size()), false); - } - } - - - static DicomMap* ComputeCounters(ServerContext& context, - const std::string& instanceId, - ResourceType level, - const DicomMap& query) - { - switch (level) - { - case ResourceType_Patient: - if (!query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) && - !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) && - !query.HasTag(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)) - { - return NULL; - } - - break; - - case ResourceType_Study: - if (!query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) && - !query.HasTag(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) && - !query.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) && - !query.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) - { - return NULL; - } - - break; - - case ResourceType_Series: - if (!query.HasTag(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES)) - { - return NULL; - } - - break; - - default: - return NULL; - } - - std::string parent; - if (!context.GetIndex().LookupParent(parent, instanceId, level)) - { - throw OrthancException(ErrorCode_UnknownResource); // The resource was deleted in between - } - - std::unique_ptr result(new DicomMap); - - switch (level) - { - case ResourceType_Patient: - ComputePatientCounters(*result, context.GetIndex(), parent, query); - break; - - case ResourceType_Study: - ComputeStudyCounters(*result, context, parent, query); - break; - - case ResourceType_Series: - ComputeSeriesCounters(*result, context.GetIndex(), parent, query); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - return result.release(); - } - - static void AddAnswer(DicomFindAnswers& answers, + ServerContext& context, + const std::string& publicId, + const std::string& instanceId, const DicomMap& mainDicomTags, const Json::Value* dicomAsJson, + ResourceType level, const DicomArray& query, const std::list& sequencesToReturn, - const DicomMap* counters, const std::string& defaultPrivateCreator, const std::map& privateCreators, const std::string& retrieveAet) { - DicomMap match; + ExpandedResource resource; + std::set requestedTags; + + query.GetTags(requestedTags); + requestedTags.erase(DICOM_TAG_QUERY_RETRIEVE_LEVEL); // this is not part of the answer - if (dicomAsJson != NULL) - { - match.FromDicomAsJson(*dicomAsJson); - } - else - { - match.Assign(mainDicomTags); - } - + // reuse ExpandResource to get missing tags and computed tags (ModalitiesInStudy ...). This code is therefore shared between C-Find, tools/find, list-resources and QIDO-RS + context.ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceDbFlags_IncludeMainDicomTags); + DicomMap result; /** @@ -330,7 +83,7 @@ else { const DicomTag& tag = query.GetElement(i).GetTag(); - const DicomValue* value = match.TestAndGetValue(tag); + const DicomValue* value = resource.tags_.TestAndGetValue(tag); if (value != NULL && !value->IsNull() && @@ -345,15 +98,6 @@ } } - if (counters != NULL) - { - DicomArray tmp(*counters); - for (size_t i = 0; i < tmp.GetSize(); i++) - { - result.SetValue(tmp.GetElement(i).GetTag(), tmp.GetElement(i).GetValue().GetContent(), false); - } - } - if (result.GetSize() == 0 && sequencesToReturn.empty()) { @@ -563,10 +307,8 @@ const DicomMap& mainDicomTags, const Json::Value* dicomAsJson) ORTHANC_OVERRIDE { - std::unique_ptr counters(ComputeCounters(context_, instanceId, level_, query_)); - - AddAnswer(answers_, mainDicomTags, dicomAsJson, queryAsArray_, sequencesToReturn_, - counters.get(), defaultPrivateCreator_, privateCreators_, retrieveAet_); + AddAnswer(answers_, context_, publicId, instanceId, mainDicomTags, dicomAsJson, level_, queryAsArray_, sequencesToReturn_, + defaultPrivateCreator_, privateCreators_, retrieveAet_); } }; diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/OrthancInitialization.cpp --- a/OrthancServer/Sources/OrthancInitialization.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -200,6 +200,80 @@ } } + static void LoadMainDicomTags(const Json::Value& configuration) + { + static const char* const EXTRA_MAIN_DICOM_TAGS = "ExtraMainDicomTags"; + + if (configuration.type() != Json::objectValue || + !configuration.isMember(EXTRA_MAIN_DICOM_TAGS) || + configuration[EXTRA_MAIN_DICOM_TAGS].type() != Json::objectValue) + { + return; + } + + Json::Value::Members levels(configuration[EXTRA_MAIN_DICOM_TAGS].getMemberNames()); + + for (Json::Value::ArrayIndex i = 0; i < levels.size(); i++) + { + ResourceType level; + if (levels[i] == "Patient") + { + level = ResourceType_Patient; + } + else if (levels[i] == "Study") + { + level = ResourceType_Study; + } + else if (levels[i] == "Series") + { + level = ResourceType_Series; + } + else if (levels[i] == "Instance") + { + level = ResourceType_Instance; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "Unknown entry '" + levels[i] + "' in ExtraMainDicomTags."); + } + + const Json::Value& content = configuration[EXTRA_MAIN_DICOM_TAGS][levels[i]]; + + if (content.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "The definition of the '" + levels[i] + "' ExtraMainDicomTags entry is invalid (not an array)."); + } + + if (content.size() > 0) + { + LOG(INFO) << "Configured Extra Main Dicom Tags for " << levels[i] << ":"; + + for (Json::Value::ArrayIndex t = 0; t < content.size(); t++) + { + const std::string& tagName = content[t].asString(); + DicomTag tag(FromDcmtkBridge::ParseTag(tagName)); + + if (DicomMap::IsComputedTag(tag)) + { + LOG(WARNING) << " - " << tagName << " can not be added in the Extra Main Dicom Tags since the value of this tag is computed when requested"; + } + else + { + ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); + if (vr == ValueRepresentation_Sequence) + { + LOG(WARNING) << " - " << tagName << " can not be added in the Extra Main Dicom Tags since it is a sequence"; + } + else + { + DicomMap::AddMainDicomTag(tag, tagName, level); + LOG(INFO) << " - " << tagName; + } + } + } + } + } + } static void ConfigurePkcs11(const Json::Value& config) { @@ -299,6 +373,10 @@ LoadExternalDictionaries(lock.GetJson()); // New in Orthanc 1.9.4 LoadCustomDictionary(lock.GetJson()); + lock.GetConfiguration().LoadWarnings(); + + LoadMainDicomTags(lock.GetJson()); // New in Orthanc 1.11.0 + lock.GetConfiguration().RegisterFont(ServerResources::FONT_UBUNTU_MONO_BOLD_16); #if HAVE_MALLOPT == 1 diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -28,6 +28,7 @@ #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/MetricsRegistry.h" #include "../../../OrthancFramework/Sources/SerializationToolbox.h" +#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../OrthancConfiguration.h" #include "../ServerContext.h" @@ -463,10 +464,12 @@ static const std::string GET_SIMPLIFY = "simplify"; static const std::string GET_FULL = "full"; static const std::string GET_SHORT = "short"; + static const std::string GET_REQUESTED_TAGS = "requestedTags"; static const std::string POST_SIMPLIFY = "Simplify"; static const std::string POST_FULL = "Full"; static const std::string POST_SHORT = "Short"; + static const std::string POST_REQUESTED_TAGS = "RequestedTags"; static const std::string DOCUMENT_SIMPLIFY = "report the DICOM tags in human-readable format (using the symbolic name of the tags)"; @@ -525,7 +528,6 @@ } } - void OrthancRestApi::DocumentDicomFormat(RestApiGetCall& call, DicomToJsonFormat defaultFormat) { @@ -570,4 +572,34 @@ "If set to `true`, " + DOCUMENT_FULL, false); } } + + void OrthancRestApi::GetRequestedTags(std::set& requestedTags, + const RestApiGetCall& call) + { + requestedTags.clear(); + + if (call.HasArgument(GET_REQUESTED_TAGS)) + { + try + { + FromDcmtkBridge::ParseListOfTags(requestedTags, call.GetArgument("requestedTags", "")); + } + catch (OrthancException& ex) + { + throw OrthancException(ErrorCode_BadRequest, std::string("Invalid requestedTags argument: ") + ex.What() + " " + ex.GetDetails()); + } + } + + } + + void OrthancRestApi::DocumentRequestedTags(RestApiGetCall& call) + { + call.GetDocumentation().SetHttpGetArgument(GET_REQUESTED_TAGS, RestApiCallDocumentation::Type_String, + "If present, list the DICOM Tags you want to list in the response. This argument is a semi-column separated list " + "of DICOM Tags identifiers; e.g: 'requestedTags=0010,0010;PatientBirthDate'. " + "The tags requested tags are returned in the 'RequestedTags' field in the response. " + "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response " + "might be slow since Orthanc will need to access the DICOM files. If not specified, Orthanc will return ", false); + } + } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h Mon Apr 25 15:50:57 2022 +0200 @@ -145,5 +145,10 @@ static void DocumentDicomFormat(RestApiPostCall& call, DicomToJsonFormat defaultFormat); + + static void GetRequestedTags(std::set& requestedTags, + const RestApiGetCall& call); + + static void DocumentRequestedTags(RestApiGetCall& call); }; } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -44,7 +44,7 @@ // This "include" is mandatory for Release builds using Linux Standard Base #include - +#include /** * This semaphore is used to limit the number of concurrent HTTP @@ -126,11 +126,15 @@ // List all the patients, studies, series or instances ---------------------- static void AnswerListOfResources(RestApiOutput& output, - ServerIndex& index, + ServerContext& context, const std::list& resources, + const std::map& instancesIds, // optional: the id of an instance for each found resource. + const std::map >& resourcesMainDicomTags, // optional: all tags read from DB for a resource (current level and upper levels) + const std::map& resourcesDicomAsJson, // optional: the dicom-as-json for each resource ResourceType level, bool expand, - DicomToJsonFormat format) + DicomToJsonFormat format, + const std::set& requestedTags) { Json::Value answer = Json::arrayValue; @@ -140,7 +144,7 @@ if (expand) { Json::Value expanded; - if (index.ExpandResource(expanded, *resource, level, format)) + if (context.ExpandResource(expanded, *resource, level, format, requestedTags)) { answer.append(expanded); } @@ -155,12 +159,29 @@ } + static void AnswerListOfResources(RestApiOutput& output, + ServerContext& context, + const std::list& resources, + ResourceType level, + bool expand, + DicomToJsonFormat format, + const std::set& requestedTags) + { + std::map unusedInstancesIds; + std::map > unusedResourcesMainDicomTags; + std::map unusedResourcesDicomAsJson; + + AnswerListOfResources(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags); + } + + template static void ListResources(RestApiGetCall& call) { if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human); + OrthancRestApi::DocumentRequestedTags(call); const std::string resources = GetResourceTypeText(resourceType, true /* plural */, false /* lower case */); call.GetDocumentation() @@ -178,9 +199,13 @@ } ServerIndex& index = OrthancRestApi::GetIndex(call); + ServerContext& context = OrthancRestApi::GetContext(call); std::list result; + std::set requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + if (call.HasArgument("limit") || call.HasArgument("since")) { @@ -207,8 +232,9 @@ index.GetAllUuids(result, resourceType); } - AnswerListOfResources(call.GetOutput(), index, result, resourceType, call.HasArgument("expand"), - OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human)); + AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand"), + OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), + requestedTags); } @@ -219,6 +245,7 @@ if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human); + OrthancRestApi::DocumentRequestedTags(call); const std::string resource = GetResourceTypeText(resourceType, false /* plural */, false /* lower case */); call.GetDocumentation() @@ -233,9 +260,12 @@ const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); + std::set requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + Json::Value json; - if (OrthancRestApi::GetIndex(call).ExpandResource( - json, call.GetUriComponent("id", ""), resourceType, format)) + if (OrthancRestApi::GetContext(call).ExpandResource( + json, call.GetUriComponent("id", ""), resourceType, format, requestedTags)) { call.GetOutput().AnswerJson(json); } @@ -2817,6 +2847,12 @@ private: bool isComplete_; std::list resources_; + + // cache the data we used during lookup and that we could reuse when building the answers + std::map instancesIds_; // the id of an instance for each found resource. + std::map > resourcesMainDicomTags_; // all tags read from DB for a resource (current level and upper levels) + std::map resourcesDicomAsJson_; // the dicom-as-json for a resource + DicomToJsonFormat format_; public: @@ -2837,19 +2873,23 @@ } virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags /* unused */, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE + const std::string& instanceId, + const DicomMap& mainDicomTags, + const Json::Value* dicomAsJson) ORTHANC_OVERRIDE { resources_.push_back(publicId); + instancesIds_[publicId] = instanceId; + resourcesMainDicomTags_[publicId].reset(mainDicomTags.Clone()); + resourcesDicomAsJson_[publicId] = dicomAsJson; } void Answer(RestApiOutput& output, - ServerIndex& index, + ServerContext& context, ResourceType level, - bool expand) const + bool expand, + const std::set& requestedTags) const { - AnswerListOfResources(output, index, resources_, level, expand, format_); + AnswerListOfResources(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags); } }; } @@ -2862,6 +2902,7 @@ static const char* const KEY_LEVEL = "Level"; static const char* const KEY_LIMIT = "Limit"; static const char* const KEY_QUERY = "Query"; + static const char* const KEY_REQUESTED_TAGS = "RequestedTags"; static const char* const KEY_SINCE = "Since"; if (call.IsDocumentation()) @@ -2884,6 +2925,12 @@ "Limit the number of reported resources", false) .SetRequestField(KEY_SINCE, RestApiCallDocumentation::Type_Number, "Show only the resources since the provided index (in conjunction with `Limit`)", false) + .SetRequestField(KEY_REQUESTED_TAGS, RestApiCallDocumentation::Type_JsonListOfStrings, + "A list of DICOM tags to include in the response (applicable only if \"Expand\" is set to true). " + "The tags requested tags are returned in the 'RequestedTags' field in the response. " + "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response " + "might be slow since Orthanc will need to access the DICOM files. If not specified, Orthanc will return " + "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false) .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject, "Associative array containing the filter on the values of the DICOM tags", true) .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information " @@ -2930,6 +2977,12 @@ throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_SINCE) + "\" should be an integer"); } + else if (request.isMember(KEY_REQUESTED_TAGS) && + request[KEY_REQUESTED_TAGS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" should be an array"); + } else { bool expand = false; @@ -2970,6 +3023,13 @@ since = static_cast(tmp); } + std::set requestedTags; + + if (request.isMember(KEY_REQUESTED_TAGS)) + { + FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]); + } + ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); DatabaseLookup query; @@ -2997,7 +3057,7 @@ FindVisitor visitor(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human)); context.Apply(visitor, query, level, since, limit); - visitor.Answer(call.GetOutput(), context.GetIndex(), level, expand); + visitor.Answer(call.GetOutput(), context, level, expand, requestedTags); } } @@ -3009,6 +3069,7 @@ if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human); + OrthancRestApi::DocumentRequestedTags(call); const std::string children = GetResourceTypeText(end, true /* plural */, false /* lower case */); const std::string resource = GetResourceTypeText(start, false /* plural */, false /* lower case */); @@ -3025,6 +3086,9 @@ ServerIndex& index = OrthancRestApi::GetIndex(call); + std::set requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + std::list a, b, c; a.push_back(call.GetUriComponent("id", "")); @@ -3054,7 +3118,7 @@ it = a.begin(); it != a.end(); ++it) { Json::Value resource; - if (OrthancRestApi::GetIndex(call).ExpandResource(resource, *it, end, format)) + if (OrthancRestApi::GetContext(call).ExpandResource(resource, *it, end, format, requestedTags)) { result.append(resource); } @@ -3132,6 +3196,7 @@ if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human); + OrthancRestApi::DocumentRequestedTags(call); const std::string parent = GetResourceTypeText(end, false /* plural */, false /* lower case */); const std::string resource = GetResourceTypeText(start, false /* plural */, false /* lower case */); @@ -3147,7 +3212,10 @@ } ServerIndex& index = OrthancRestApi::GetIndex(call); - + + std::set requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + std::string current = call.GetUriComponent("id", ""); ResourceType currentType = start; while (currentType > end) @@ -3169,7 +3237,7 @@ const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); Json::Value resource; - if (OrthancRestApi::GetIndex(call).ExpandResource(resource, current, end, format)) + if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags)) { call.GetOutput().AnswerJson(resource); } @@ -3408,7 +3476,7 @@ { static const char* const LEVEL = "Level"; static const char* const METADATA = "Metadata"; - + if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Human); @@ -3420,7 +3488,7 @@ "List of the Orthanc identifiers of the patients/studies/series/instances of interest.", true) .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 " + "`Instance`). Orthanc will loop over the items inside `Resources`, and explore 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) @@ -3541,7 +3609,9 @@ it = interest.begin(); it != interest.end(); ++it) { Json::Value item; - if (index.ExpandResource(item, *it, level, format)) + std::set emptyRequestedTags; // not supported for bulk content + + if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags)) { if (metadata) { @@ -3563,8 +3633,10 @@ { ResourceType level; Json::Value item; + std::set emptyRequestedTags; // not supported for bulk content + if (index.LookupResourceType(level, *it) && - index.ExpandResource(item, *it, level, format)) + OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags)) { if (metadata) { diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -47,7 +47,17 @@ { call.GetOutput().Redirect("app/images/favicon.ico"); } - + + static void GetMainDicomTagsConfiguration(Json::Value& result) + { + Json::Value v; + + result["Patient"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Patient); + result["Study"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Study); + result["Series"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Series); + result["Instance"] = DicomMap::GetMainDicomTagsSignature(ResourceType_Instance); + } + static void GetSystemInformation(RestApiGetCall& call) { static const char* const API_VERSION = "ApiVersion"; @@ -62,6 +72,8 @@ static const char* const PLUGINS_ENABLED = "PluginsEnabled"; static const char* const STORAGE_AREA_PLUGIN = "StorageAreaPlugin"; static const char* const VERSION = "Version"; + static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; + static const char* const STORAGE_COMPRESSION = "StorageCompression"; if (call.IsDocumentation()) { @@ -88,6 +100,10 @@ "Whether Orthanc was built with support for plugins") .SetAnswerField(CHECK_REVISIONS, RestApiCallDocumentation::Type_Boolean, "Whether Orthanc handle revisions of metadata and attachments to deal with multiple writers (new in Orthanc 1.9.2)") + .SetAnswerField(MAIN_DICOM_TAGS, RestApiCallDocumentation::Type_JsonObject, + "The list of MainDicomTags saved in DB for each resource level (new in Orthanc 1.11.0)") + .SetAnswerField(STORAGE_COMPRESSION, RestApiCallDocumentation::Type_Boolean, + "Whether storage compression is enabled (new in Orthanc 1.11.0)") .SetHttpGetSample("https://demo.orthanc-server.com/system", true); return; } @@ -108,6 +124,7 @@ result[HTTP_PORT] = lock.GetConfiguration().GetUnsignedIntegerParameter(HTTP_PORT, 8042); result[NAME] = lock.GetConfiguration().GetStringParameter(NAME, ""); result[CHECK_REVISIONS] = lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false); // New in Orthanc 1.9.2 + result[STORAGE_COMPRESSION] = lock.GetConfiguration().GetBooleanParameter(STORAGE_COMPRESSION, false); // New in Orthanc 1.11.0 } result[STORAGE_AREA_PLUGIN] = Json::nullValue; @@ -132,6 +149,9 @@ result[PLUGINS_ENABLED] = false; #endif + result[MAIN_DICOM_TAGS] = Json::objectValue; + GetMainDicomTagsConfiguration(result[MAIN_DICOM_TAGS]); + call.GetOutput().AnswerJson(result); } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/OrthancWebDav.cpp --- a/OrthancServer/Sources/OrthancWebDav.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/OrthancWebDav.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -259,7 +259,9 @@ const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE { Json::Value resource; - if (context_.GetIndex().ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human)) + std::set emptyRequestedTags; // not supported for webdav + + if (context_.ExpandResource(resource, publicId, level_, DicomToJsonFormat_Human, emptyRequestedTags)) { if (success_) { diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/Search/DatabaseLookup.cpp --- a/OrthancServer/Sources/Search/DatabaseLookup.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -281,14 +281,13 @@ bool DatabaseLookup::HasOnlyMainDicomTags() const { - std::set mainTags; - DicomMap::GetMainDicomTags(mainTags); + const std::set& allMainTags = DicomMap::GetAllMainDicomTags(); for (size_t i = 0; i < constraints_.size(); i++) { assert(constraints_[i] != NULL); - if (mainTags.find(constraints_[i]->GetTag()) == mainTags.end()) + if (allMainTags.find(constraints_[i]->GetTag()) == allMainTags.end()) { // This is not a main DICOM tag return false; @@ -327,4 +326,45 @@ return false; } + + bool DatabaseLookup::GetConstraint(const DicomTagConstraint*& constraint, const DicomTag& tag) const + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + if (constraints_[i]->GetTag() == tag) + { + constraint = constraints_.at(i); + return true; + } + } + + return false; + } + + + void DatabaseLookup::RemoveConstraint(const DicomTag& tag) + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + if (constraints_[i]->GetTag() == tag) + { + delete constraints_[i]; + constraints_.erase(constraints_.begin() + i); + } + } + } + + DatabaseLookup* DatabaseLookup::Clone() const + { + std::unique_ptr clone(new DatabaseLookup()); + + for (size_t i = 0; i < constraints_.size(); i++) + { + clone->AddConstraint(*(new DicomTagConstraint(*constraints_[i]))); + } + + return clone.release(); + } } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/Search/DatabaseLookup.h --- a/OrthancServer/Sources/Search/DatabaseLookup.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.h Mon Apr 25 15:50:57 2022 +0200 @@ -48,6 +48,8 @@ ~DatabaseLookup(); + DatabaseLookup* Clone() const; + void Reserve(size_t n) { constraints_.reserve(n); @@ -60,6 +62,8 @@ const DicomTagConstraint& GetConstraint(size_t index) const; + bool GetConstraint(const DicomTagConstraint*& constraint, const DicomTag& tag) const; + bool IsMatch(const DicomMap& value) const; bool IsMatch(DcmItem& item, @@ -86,5 +90,7 @@ std::string Format() const; bool HasTag(const DicomTag& tag) const; + + void RemoveConstraint(const DicomTag& tag); }; } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/Search/DicomTagConstraint.cpp --- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -214,7 +214,7 @@ } - bool DicomTagConstraint::IsMatch(const std::string& value) + bool DicomTagConstraint::IsMatch(const std::string& value) const { NormalizedString source(value, caseSensitive_); @@ -269,7 +269,7 @@ } - bool DicomTagConstraint::IsMatch(const DicomMap& value) + bool DicomTagConstraint::IsMatch(const DicomMap& value) const { const DicomValue* tmp = value.TestAndGetValue(tag_); diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/Search/DicomTagConstraint.h --- a/OrthancServer/Sources/Search/DicomTagConstraint.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.h Mon Apr 25 15:50:57 2022 +0200 @@ -42,7 +42,7 @@ bool caseSensitive_; bool mandatory_; - boost::shared_ptr regex_; + mutable boost::shared_ptr regex_; // mutable because the regex is an internal object created only when required (in IsMatch const method) void AssignSingleValue(const std::string& value); @@ -102,9 +102,9 @@ return values_; } - bool IsMatch(const std::string& value); + bool IsMatch(const std::string& value) const; - bool IsMatch(const DicomMap& value); + bool IsMatch(const DicomMap& value) const; std::string Format() const; diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -64,6 +64,12 @@ namespace Orthanc { + static void ComputeStudyTags(ExpandedResource& resource, + ServerContext& context, + const std::string& studyPublicId, + const std::set& requestedTags); + + static bool IsUncompressedTransferSyntax(DicomTransferSyntax transferSyntax) { return (transferSyntax == DicomTransferSyntax_LittleEndianImplicit || @@ -1354,20 +1360,33 @@ } - void ServerContext::ApplyInternal(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - size_t since, - size_t limit) + void ServerContext::Apply(ILookupVisitor& visitor, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t since, + size_t limit) { unsigned int databaseLimit = (queryLevel == ResourceType_Instance ? limitFindInstances_ : limitFindResults_); std::vector resources, instances; + const DicomTagConstraint* dicomModalitiesConstraint = NULL; + + bool hasModalitiesInStudyLookup = (queryLevel == ResourceType_Study && + lookup.GetConstraint(dicomModalitiesConstraint, DICOM_TAG_MODALITIES_IN_STUDY) && + ((dicomModalitiesConstraint->GetConstraintType() == ConstraintType_Equal && !dicomModalitiesConstraint->GetValue().empty()) || + (dicomModalitiesConstraint->GetConstraintType() == ConstraintType_List && !dicomModalitiesConstraint->GetValues().empty()))); + + std::unique_ptr fastLookup(lookup.Clone()); + + if (hasModalitiesInStudyLookup) + { + fastLookup->RemoveConstraint(DICOM_TAG_MODALITIES_IN_STUDY); + } { const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1); - GetIndex().ApplyLookupResources(resources, &instances, lookup, queryLevel, lookupLimit); + GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, lookupLimit); } bool complete = (databaseLimit == 0 || @@ -1396,16 +1415,16 @@ bool hasOnlyMainDicomTags; DicomMap dicom; + DicomMap allMainDicomTagsFromDB; if (findStorageAccessMode_ == FindStorageAccessMode_DatabaseOnly || findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer || - lookup.HasOnlyMainDicomTags()) + fastLookup->HasOnlyMainDicomTags()) { // Case (1): The main DICOM tags, as stored in the database, // are sufficient to look for match - DicomMap tmp; - if (!GetIndex().GetAllMainDicomTags(tmp, instances[i])) + if (!GetIndex().GetAllMainDicomTags(allMainDicomTagsFromDB, instances[i])) { // The instance has been removed during the execution of the // lookup, ignore it @@ -1418,16 +1437,16 @@ { // WARNING: Don't reorder cases below, and don't add "break" case ResourceType_Instance: - dicom.MergeMainDicomTags(tmp, ResourceType_Instance); + dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Instance); case ResourceType_Series: - dicom.MergeMainDicomTags(tmp, ResourceType_Series); + dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Series); case ResourceType_Study: - dicom.MergeMainDicomTags(tmp, ResourceType_Study); + dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Study); case ResourceType_Patient: - dicom.MergeMainDicomTags(tmp, ResourceType_Patient); + dicom.MergeMainDicomTags(allMainDicomTagsFromDB, ResourceType_Patient); break; default: @@ -1449,46 +1468,71 @@ hasOnlyMainDicomTags = false; } - if (lookup.IsMatch(dicom)) + if (fastLookup->IsMatch(dicom)) { - if (skipped < since) - { - skipped++; - } - else if (limit != 0 && - countResults >= limit) + bool isMatch = true; + + if (hasModalitiesInStudyLookup) { - // Too many results, don't mark as complete - complete = false; - break; - } - else - { - if ((findStorageAccessMode_ == FindStorageAccessMode_DiskOnLookupAndAnswer || - findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer) && - dicomAsJson.get() == NULL && - isDicomAsJsonNeeded) + std::set requestedTags; + requestedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); + ExpandedResource resource; + ComputeStudyTags(resource, *this, resources[i], requestedTags); + + std::vector modalities; + Toolbox::TokenizeString(modalities, resource.tags_.GetValue(DICOM_TAG_MODALITIES_IN_STUDY).GetContent(), '\\'); + bool hasAtLeastOneModalityMatching = false; + for (size_t m = 0; m < modalities.size(); m++) { - dicomAsJson.reset(new Json::Value); - ReadDicomAsJson(*dicomAsJson, instances[i]); + hasAtLeastOneModalityMatching |= dicomModalitiesConstraint->IsMatch(modalities[m]); } - if (hasOnlyMainDicomTags) + isMatch = isMatch && hasAtLeastOneModalityMatching; + // copy the value of ModalitiesInStudy such that it can be reused to build the answer + allMainDicomTagsFromDB.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, resource.tags_.GetValue(DICOM_TAG_MODALITIES_IN_STUDY)); + } + + if (isMatch) + { + if (skipped < since) { - // This is Case (1): The variable "dicom" only contains the main DICOM tags - visitor.Visit(resources[i], instances[i], dicom, dicomAsJson.get()); + skipped++; + } + else if (limit != 0 && + countResults >= limit) + { + // Too many results, don't mark as complete + complete = false; + break; } else { - // Remove the non-main DICOM tags from "dicom" if Case (2) - // was used, for consistency with Case (1) + if ((findStorageAccessMode_ == FindStorageAccessMode_DiskOnLookupAndAnswer || + findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer) && + dicomAsJson.get() == NULL && + isDicomAsJsonNeeded) + { + dicomAsJson.reset(new Json::Value); + ReadDicomAsJson(*dicomAsJson, instances[i]); + } - DicomMap mainDicomTags; - mainDicomTags.ExtractMainDicomTags(dicom); - visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get()); + if (hasOnlyMainDicomTags) + { + // This is Case (1): The variable "dicom" only contains the main DICOM tags + visitor.Visit(resources[i], instances[i], allMainDicomTagsFromDB, dicomAsJson.get()); + } + else + { + // Remove the non-main DICOM tags from "dicom" if Case (2) + // was used, for consistency with Case (1) + + DicomMap mainDicomTags; + mainDicomTags.ExtractMainDicomTags(dicom); + visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get()); + } + + countResults ++; } - - countResults ++; } } } @@ -1501,228 +1545,6 @@ LOG(INFO) << "Number of matching resources: " << countResults; } - - - namespace - { - class ModalitiesInStudyVisitor : public ServerContext::ILookupVisitor - { - private: - class Study : public boost::noncopyable - { - private: - std::string orthancId_; - std::string instanceId_; - DicomMap mainDicomTags_; - Json::Value dicomAsJson_; - std::set modalitiesInStudy_; - - public: - Study(const std::string& instanceId, - const DicomMap& seriesTags) : - instanceId_(instanceId), - dicomAsJson_(Json::nullValue) - { - { - DicomMap tmp; - tmp.Assign(seriesTags); - tmp.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "dummy", false); - DicomInstanceHasher hasher(tmp); - orthancId_ = hasher.HashStudy(); - } - - mainDicomTags_.MergeMainDicomTags(seriesTags, ResourceType_Study); - mainDicomTags_.MergeMainDicomTags(seriesTags, ResourceType_Patient); - AddModality(seriesTags); - } - - void AddModality(const DicomMap& seriesTags) - { - std::string modality; - if (seriesTags.LookupStringValue(modality, DICOM_TAG_MODALITY, false) && - !modality.empty()) - { - modalitiesInStudy_.insert(modality); - } - } - - void SetDicomAsJson(const Json::Value& dicomAsJson) - { - dicomAsJson_ = dicomAsJson; - } - - const std::string& GetOrthancId() const - { - return orthancId_; - } - - const std::string& GetInstanceId() const - { - return instanceId_; - } - - const DicomMap& GetMainDicomTags() const - { - return mainDicomTags_; - } - - const Json::Value* GetDicomAsJson() const - { - if (dicomAsJson_.type() == Json::nullValue) - { - return NULL; - } - else - { - return &dicomAsJson_; - } - } - }; - - typedef std::map Studies; - - bool isDicomAsJsonNeeded_; - bool complete_; - Studies studies_; - - public: - explicit ModalitiesInStudyVisitor(bool isDicomAsJsonNeeded) : - isDicomAsJsonNeeded_(isDicomAsJsonNeeded), - complete_(false) - { - } - - ~ModalitiesInStudyVisitor() - { - for (Studies::const_iterator it = studies_.begin(); it != studies_.end(); ++it) - { - assert(it->second != NULL); - delete it->second; - } - - studies_.clear(); - } - - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return isDicomAsJsonNeeded_; - } - - virtual void MarkAsComplete() ORTHANC_OVERRIDE - { - complete_ = true; - } - - virtual void Visit(const std::string& publicId, - const std::string& instanceId, - const DicomMap& seriesTags, - const Json::Value* dicomAsJson) ORTHANC_OVERRIDE - { - std::string studyInstanceUid; - if (seriesTags.LookupStringValue(studyInstanceUid, DICOM_TAG_STUDY_INSTANCE_UID, false)) - { - Studies::iterator found = studies_.find(studyInstanceUid); - if (found == studies_.end()) - { - // New study - std::unique_ptr study(new Study(instanceId, seriesTags)); - - if (dicomAsJson != NULL) - { - study->SetDicomAsJson(*dicomAsJson); - } - - studies_[studyInstanceUid] = study.release(); - } - else - { - // Already existing study - found->second->AddModality(seriesTags); - } - } - } - - void Forward(ILookupVisitor& callerVisitor, - size_t since, - size_t limit) const - { - size_t index = 0; - size_t countForwarded = 0; - - for (Studies::const_iterator it = studies_.begin(); it != studies_.end(); ++it, index++) - { - if (limit == 0 || - (index >= since && - index < limit)) - { - assert(it->second != NULL); - const Study& study = *it->second; - - countForwarded++; - callerVisitor.Visit(study.GetOrthancId(), study.GetInstanceId(), - study.GetMainDicomTags(), study.GetDicomAsJson()); - } - } - - if (countForwarded == studies_.size()) - { - callerVisitor.MarkAsComplete(); - } - } - }; - } - - - void ServerContext::Apply(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - size_t since, - size_t limit) - { - if (queryLevel == ResourceType_Study && - lookup.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) - { - // Convert the study-level query, into a series-level query, - // where "ModalitiesInStudy" is replaced by "Modality" - DatabaseLookup seriesLookup; - - for (size_t i = 0; i < lookup.GetConstraintsCount(); i++) - { - const DicomTagConstraint& constraint = lookup.GetConstraint(i); - if (constraint.GetTag() == DICOM_TAG_MODALITIES_IN_STUDY) - { - if ((constraint.GetConstraintType() == ConstraintType_Equal && constraint.GetValue().empty()) || - (constraint.GetConstraintType() == ConstraintType_List && constraint.GetValues().empty())) - { - // Ignore universal lookup on "ModalitiesInStudy" (0008,0061), - // this should have been handled by the caller - ApplyInternal(visitor, lookup, queryLevel, since, limit); - return; - } - else - { - DicomTagConstraint modality(constraint); - modality.SetTag(DICOM_TAG_MODALITY); - seriesLookup.AddConstraint(modality); - } - } - else - { - seriesLookup.AddConstraint(constraint); - } - } - - ModalitiesInStudyVisitor seriesVisitor(visitor.IsDicomAsJsonNeeded()); - ApplyInternal(seriesVisitor, seriesLookup, ResourceType_Series, 0, 0); - seriesVisitor.Forward(visitor, since, limit); - } - else - { - ApplyInternal(visitor, lookup, queryLevel, since, limit); - } - } - - bool ServerContext::LookupOrReconstructMetadata(std::string& target, const std::string& publicId, ResourceType level, @@ -2092,4 +1914,543 @@ boost::mutex::scoped_lock lock(dynamicOptionsMutex_); isUnknownSopClassAccepted_ = accepted; } + + + static void SerializeExpandedResource(Json::Value& target, + const ExpandedResource& resource, + DicomToJsonFormat format, + const std::set& requestedTags) + { + target = Json::objectValue; + + target["Type"] = GetResourceTypeText(resource.type_, false, true); + target["ID"] = resource.id_; + + switch (resource.type_) + { + case ResourceType_Patient: + break; + + case ResourceType_Study: + target["ParentPatient"] = resource.parentId_; + break; + + case ResourceType_Series: + target["ParentStudy"] = resource.parentId_; + break; + + case ResourceType_Instance: + target["ParentSeries"] = resource.parentId_; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + switch (resource.type_) + { + case ResourceType_Patient: + case ResourceType_Study: + case ResourceType_Series: + { + Json::Value c = Json::arrayValue; + + for (std::list::const_iterator + it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it) + { + c.append(*it); + } + + if (resource.type_ == ResourceType_Patient) + { + target["Studies"] = c; + } + else if (resource.type_ == ResourceType_Study) + { + target["Series"] = c; + } + else + { + target["Instances"] = c; + } + break; + } + + case ResourceType_Instance: + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + switch (resource.type_) + { + case ResourceType_Patient: + case ResourceType_Study: + break; + + case ResourceType_Series: + if (resource.expectedNumberOfInstances_ < 0) + { + target["ExpectedNumberOfInstances"] = Json::nullValue; + } + else + { + target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_; + } + target["Status"] = resource.status_; + break; + + case ResourceType_Instance: + { + target["FileSize"] = static_cast(resource.fileSize_); + target["FileUuid"] = resource.fileUuid_; + + if (resource.indexInSeries_ < 0) + { + target["IndexInSeries"] = Json::nullValue; + } + else + { + target["IndexInSeries"] = resource.indexInSeries_; + } + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (!resource.anonymizedFrom_.empty()) + { + target["AnonymizedFrom"] = resource.anonymizedFrom_; + } + + if (!resource.modifiedFrom_.empty()) + { + target["ModifiedFrom"] = resource.modifiedFrom_; + } + + if (resource.type_ == ResourceType_Patient || + resource.type_ == ResourceType_Study || + resource.type_ == ResourceType_Series) + { + target["IsStable"] = resource.isStable_; + + if (!resource.lastUpdate_.empty()) + { + target["LastUpdate"] = resource.lastUpdate_; + } + } + + // serialize tags + + static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; + static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags"; + + DicomMap mainDicomTags; + resource.tags_.ExtractResourceInformation(mainDicomTags, resource.type_); + + target[MAIN_DICOM_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format); + + if (resource.type_ == ResourceType_Study) + { + DicomMap patientMainDicomTags; + resource.tags_.ExtractPatientInformation(patientMainDicomTags); + + target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format); + } + + if (requestedTags.size() > 0) + { + static const char* const REQUESTED_TAGS = "RequestedTags"; + + DicomMap tags; + resource.tags_.ExtractTags(tags, requestedTags); + + target[REQUESTED_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format); + } + + } + + + static void ComputeInstanceTags(ExpandedResource& resource, + ServerContext& context, + const std::string& instancePublicId, + const std::set& requestedTags) + { + if (requestedTags.count(DICOM_TAG_INSTANCE_AVAILABILITY) > 0) + { + resource.tags_.SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false); + resource.missingRequestedTags_.erase(DICOM_TAG_INSTANCE_AVAILABILITY); + } + } + + + static void ComputeSeriesTags(ExpandedResource& resource, + ServerContext& context, + const std::string& seriesPublicId, + const std::set& requestedTags) + { + if (requestedTags.count(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) > 0) + { + ServerIndex& index = context.GetIndex(); + std::list instances; + + index.GetChildren(instances, seriesPublicId); + + resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, + boost::lexical_cast(instances.size()), false); + resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); + } + } + + static void ComputeStudyTags(ExpandedResource& resource, + ServerContext& context, + const std::string& studyPublicId, + const std::set& requestedTags) + { + ServerIndex& index = context.GetIndex(); + std::list series; + std::list instances; + + bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) > 0; + bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0; + bool hasModalitiesInStudy = requestedTags.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0; + bool hasSopClassesInStudy = requestedTags.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0; + + index.GetChildren(series, studyPublicId); + + if (hasModalitiesInStudy) + { + std::set values; + + for (std::list::const_iterator + it = series.begin(); it != series.end(); ++it) + { + DicomMap tags; + index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series); + + const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); + + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + values.insert(value->GetContent()); + } + } + + std::string modalities; + Toolbox::JoinStrings(modalities, values, "\\"); + + resource.tags_.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false); + resource.missingRequestedTags_.erase(DICOM_TAG_MODALITIES_IN_STUDY); + } + + if (hasNbRelatedSeries) + { + resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, + boost::lexical_cast(series.size()), false); + resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); + } + + if (hasNbRelatedInstances || hasSopClassesInStudy) + { + for (std::list::const_iterator + it = series.begin(); it != series.end(); ++it) + { + std::list seriesInstancesIds; + index.GetChildren(seriesInstancesIds, *it); + + instances.splice(instances.end(), seriesInstancesIds); + } + + if (hasNbRelatedInstances) + { + resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, + boost::lexical_cast(instances.size()), false); + resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); + } + + if (hasSopClassesInStudy) + { + std::set values; + + for (std::list::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + std::string value; + + if (context.LookupOrReconstructMetadata(value, *it, ResourceType_Instance, MetadataType_Instance_SopClassUid)) + { + values.insert(value); + } + } + + if (values.size() > 0) + { + std::string sopClassUids; + Toolbox::JoinStrings(sopClassUids, values, "\\"); + resource.tags_.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false); + } + + resource.missingRequestedTags_.erase(DICOM_TAG_SOP_CLASSES_IN_STUDY); + } + } + } + + static void ComputePatientTags(ExpandedResource& resource, + ServerContext& context, + const std::string& patientPublicId, + const std::set& requestedTags) + { + ServerIndex& index = context.GetIndex(); + + std::list studies; + std::list series; + std::list instances; + + bool hasNbRelatedStudies = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) > 0; + bool hasNbRelatedSeries = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) > 0; + bool hasNbRelatedInstances = requestedTags.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) > 0; + + index.GetChildren(studies, patientPublicId); + + if (hasNbRelatedStudies) + { + resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, + boost::lexical_cast(studies.size()), false); + resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); + } + + if (hasNbRelatedSeries || hasNbRelatedInstances) + { + for (std::list::const_iterator + it = studies.begin(); it != studies.end(); ++it) + { + std::list thisSeriesIds; + index.GetChildren(thisSeriesIds, *it); + series.splice(series.end(), thisSeriesIds); + } + + if (hasNbRelatedSeries) + { + resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, + boost::lexical_cast(series.size()), false); + resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); + } + } + + if (hasNbRelatedInstances) + { + for (std::list::const_iterator + it = series.begin(); it != series.end(); ++it) + { + std::list thisInstancesIds; + index.GetChildren(thisInstancesIds, *it); + instances.splice(instances.end(), thisInstancesIds); + } + + resource.tags_.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, + boost::lexical_cast(instances.size()), false); + resource.missingRequestedTags_.erase(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); + } + } + + + static void ComputeTags(ExpandedResource& resource, + ServerContext& context, + const std::string& resourceId, + ResourceType level, + const std::set& requestedTags) + { + if (level == ResourceType_Patient + && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Patient)) + { + ComputePatientTags(resource, context, resourceId, requestedTags); + } + + if (level == ResourceType_Study + && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Study)) + { + ComputeStudyTags(resource, context, resourceId, requestedTags); + } + + if (level == ResourceType_Series + && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Series)) + { + ComputeSeriesTags(resource, context, resourceId, requestedTags); + } + + if (level == ResourceType_Instance + && DicomMap::HasComputedTags(resource.missingRequestedTags_, ResourceType_Instance)) + { + ComputeInstanceTags(resource, context, resourceId, requestedTags); + } + } + + bool ServerContext::ExpandResource(Json::Value& target, + const std::string& publicId, + ResourceType level, + DicomToJsonFormat format, + const std::set& requestedTags) + { + std::string unusedInstanceId; + Json::Value* unusedDicomAsJson = NULL; + DicomMap unusedMainDicomTags; + + return ExpandResource(target, publicId, unusedMainDicomTags, unusedInstanceId, unusedDicomAsJson, level, format, requestedTags); + } + + bool ServerContext::ExpandResource(Json::Value& target, + const std::string& publicId, + const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) + const std::string& instanceId, // optional: the id of an instance for the resource (if already available) + const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource (if already available) + ResourceType level, + DicomToJsonFormat format, + const std::set& requestedTags) + { + ExpandedResource resource; + + if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceDbFlags_Default)) + { + SerializeExpandedResource(target, resource, format, requestedTags); + return true; + } + + return false; + } + + bool ServerContext::ExpandResource(ExpandedResource& resource, + const std::string& publicId, + const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) + const std::string& instanceId, // optional: the id of an instance for the resource (if already available) + const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource (if already available) + ResourceType level, + const std::set& requestedTags, + ExpandResourceDbFlags expandFlags) + { + // first try to get the tags from what is already available + + if ((expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags) + && (mainDicomTags.GetSize() > 0) + && (dicomAsJson != NULL)) + { + + if (mainDicomTags.GetSize() > 0) + { + resource.tags_.Merge(mainDicomTags); + } + + if (dicomAsJson != NULL && dicomAsJson->isObject()) + { + resource.tags_.FromDicomAsJson(*dicomAsJson); + } + + std::set retrievedTags; + std::set missingTags; + resource.tags_.GetTags(retrievedTags); + + Toolbox::GetMissingsFromSet(missingTags, requestedTags, retrievedTags); + + // if all possible tags have been read, no need to get them from DB anymore + if (missingTags.size() == 0 || DicomMap::HasOnlyComputedTags(missingTags)) + { + expandFlags = static_cast(expandFlags & ~ExpandResourceDbFlags_IncludeMainDicomTags); + } + + if (missingTags.size() == 0 && expandFlags == ExpandResourceDbFlags_None) // we have already retrieved anything we need + { + return true; + } + } + + if (expandFlags != ExpandResourceDbFlags_None + && GetIndex().ExpandResource(resource, publicId, level, requestedTags, static_cast(expandFlags | ExpandResourceDbFlags_IncludeMetadata))) // we always need the metadata to get the mainDicomTagsSignature + { + // check the main dicom tags list has not changed since the resource was stored + if (resource.mainDicomTagsSignature_ != DicomMap::GetMainDicomTagsSignature(resource.type_)) + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().IsWarningEnabled(Warnings_002_InconsistentDicomTagsInDb)) + { + LOG(WARNING) << "W002: " << Orthanc::GetResourceTypeText(resource.type_, false , false) << " has been stored with another version of Main Dicom Tags list, you should POST to /" << Orthanc::GetResourceTypeText(resource.type_, true, false) << "/" << resource.id_ << "/reconstruct to update the list of tags saved in DB. Some MainDicomTags might be missing from this answer."; + } + } + + // possibly merge missing requested tags from dicom-as-json + if (!resource.missingRequestedTags_.empty() && !DicomMap::HasOnlyComputedTags(resource.missingRequestedTags_)) + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage)) + { + std::set missingTags; + Toolbox::AppendSets(missingTags, resource.missingRequestedTags_); + for (std::set::const_iterator it = resource.missingRequestedTags_.begin(); it != resource.missingRequestedTags_.end(); it++) + { + if (DicomMap::IsComputedTag(*it)) + { + missingTags.erase(*it); + } + } + + std::string missings; + FromDcmtkBridge::FormatListOfTags(missings, missingTags); + + LOG(WARNING) << "W001: Accessing Dicom tags from storage when accessing " << Orthanc::GetResourceTypeText(resource.type_, false , false) << " : " << missings; + } + + + std::string instanceId_ = instanceId; + DicomMap tagsFromJson; + + if (dicomAsJson == NULL) + { + if (instanceId_.empty()) + { + if (level == ResourceType_Instance) + { + instanceId_ = publicId; + } + else + { + std::list instancesIds; + GetIndex().GetChildInstances(instancesIds, publicId); + if (instancesIds.size() < 1) + { + throw OrthancException(ErrorCode_InternalError, "ExpandResource: no instances found"); + } + instanceId_ = instancesIds.front(); + } + } + + Json::Value tmpDicomAsJson; + ReadDicomAsJson(tmpDicomAsJson, instanceId_); + tagsFromJson.FromDicomAsJson(tmpDicomAsJson); + } + else + { + tagsFromJson.FromDicomAsJson(*dicomAsJson); + } + + resource.tags_.Merge(tagsFromJson); + } + + // compute the requested tags + ComputeTags(resource, *this, publicId, level, requestedTags); + } + else + { + return false; + } + + return true; + } + } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/ServerContext.h Mon Apr 25 15:50:57 2022 +0200 @@ -263,12 +263,6 @@ DicomInstanceToStore& dicom, StoreInstanceMode mode); - void ApplyInternal(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - size_t since, - size_t limit); - void PublishDicomCacheMetrics(); // This method must only be called from "ServerIndex"! @@ -539,5 +533,30 @@ bool IsUnknownSopClassAccepted(); void SetUnknownSopClassAccepted(bool accepted); + + bool ExpandResource(Json::Value& target, + const std::string& publicId, + ResourceType level, + DicomToJsonFormat format, + const std::set& requestedTags); + + bool ExpandResource(Json::Value& target, + const std::string& publicId, + const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) + const std::string& instanceId, // optional: the id of an instance for the resource + const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource + ResourceType level, + DicomToJsonFormat format, + const std::set& requestedTags); + + bool ExpandResource(ExpandedResource& target, + const std::string& publicId, + const DicomMap& mainDicomTags, // optional: the main dicom tags for the resource (if already available) + const std::string& instanceId, // optional: the id of an instance for the resource + const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource + ResourceType level, + const std::set& requestedTags, + ExpandResourceDbFlags expandFlags); + }; } diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/ServerEnumerations.cpp --- a/OrthancServer/Sources/ServerEnumerations.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -60,6 +60,7 @@ dictMetadataType_.Add(MetadataType_Instance_CalledAet, "CalledAET"); dictMetadataType_.Add(MetadataType_Instance_HttpUsername, "HttpUsername"); dictMetadataType_.Add(MetadataType_Instance_PixelDataOffset, "PixelDataOffset"); + dictMetadataType_.Add(MetadataType_MainDicomTagsSignature, "MainDicomTagsSignature"); dictContentType_.Add(FileContentType_Dicom, "dicom"); dictContentType_.Add(FileContentType_DicomAsJson, "dicom-as-json"); diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/ServerEnumerations.h --- a/OrthancServer/Sources/ServerEnumerations.h Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.h Mon Apr 25 15:50:57 2022 +0200 @@ -151,7 +151,8 @@ MetadataType_Instance_CalledAet = 12, // New in Orthanc 1.4.0 MetadataType_Instance_HttpUsername = 13, // New in Orthanc 1.4.0 MetadataType_Instance_PixelDataOffset = 14, // New in Orthanc 1.9.0 - + MetadataType_MainDicomTagsSignature = 15, // New in Orthanc 1.11.0 + // Make sure that the value "65535" can be stored into this enumeration MetadataType_StartUser = 1024, MetadataType_EndUser = 65535 @@ -190,6 +191,12 @@ BuiltinDecoderTranscoderOrder_Disabled }; + enum Warnings + { + Warnings_None, + Warnings_001_TagsBeingReadFromStorage, + Warnings_002_InconsistentDicomTagsInDb, + }; void InitializeServerEnumerations(); diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/ServerToolbox.cpp --- a/OrthancServer/Sources/ServerToolbox.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/ServerToolbox.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -107,29 +107,7 @@ // example). Take this improvement into consideration for the // next upgrade of the database schema. - const char* plural = NULL; - - switch (level) - { - case ResourceType_Patient: - plural = "patients"; - break; - - case ResourceType_Study: - plural = "studies"; - break; - - case ResourceType_Series: - plural = "series"; - break; - - case ResourceType_Instance: - plural = "instances"; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } + const char* plural = Orthanc::GetResourceTypeText(level, true, true); LOG(WARNING) << "Upgrade: Reconstructing the main DICOM tags of all the " << plural << "..."; @@ -183,6 +161,9 @@ ResourcesContent tags(false /* prevent the setting of metadata */); tags.AddResource(resource, level, dicomSummary); transaction.SetResourcesContent(tags); + + transaction.DeleteMetadata(resource, MetadataType_MainDicomTagsSignature); + transaction.SetMetadata(resource, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(level), 0); } catch (OrthancException&) { diff -r 5e7404f23fa8 -r 8b95fc86b8d9 OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Wed Apr 13 10:51:22 2022 +0200 +++ b/OrthancServer/Sources/main.cpp Mon Apr 25 15:50:57 2022 +0200 @@ -770,7 +770,7 @@ << std::endl << std::endl; // The content of the following brackets is automatically generated - // by the "GenerateErrorCodes.py" script + // by the "Resources/CodeGeneration/GenerateErrorCodes.py" script { PrintErrorCode(ErrorCode_InternalError, "Internal error"); PrintErrorCode(ErrorCode_Success, "Success"); @@ -817,6 +817,7 @@ PrintErrorCode(ErrorCode_BadRange, "Incorrect range request"); PrintErrorCode(ErrorCode_DatabaseCannotSerialize, "Database could not serialize access due to concurrent update, the transaction should be retried"); PrintErrorCode(ErrorCode_Revision, "A bad revision number was provided, which might indicate conflict between multiple writers"); + PrintErrorCode(ErrorCode_MainDicomTagsMultiplyDefined, "A main DICOM Tag has been defined multiple times for the same resource level"); PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened"); PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open"); PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database"); diff -r 5e7404f23fa8 -r 8b95fc86b8d9 TODO --- a/TODO Wed Apr 13 10:51:22 2022 +0200 +++ b/TODO Mon Apr 25 15:50:57 2022 +0200 @@ -7,7 +7,7 @@ https://book.orthanc-server.com/contributing.html -Some features are being funded by and OpenCollective one-time donations. +Some features are being funded by an OpenCollective one-time donations. selected features are marked with priorities ((1) - higher, (2) - medium, (3) - nice to have) ======= @@ -122,9 +122,6 @@ - On SCP side: done by https://hg.orthanc-server.com/orthanc/rev/1ec3e1e18f50 - On SCU side: https://groups.google.com/d/msg/orthanc-users/wPl0g5mqZco/5X1Z8tEzBgAJ -* Support "Instance Availability" (0008,0056) in C-FIND: - http://dicom.nema.org/medical/DICOM/2019a/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3.2 - https://groups.google.com/d/msg/orthanc-users/hteDgE6igo8/j-ArqD7pBQAJ * Check Big Endian transfer syntax in ParsedDicomFile::EmbedImage and DicomImageDecoder * Strict hierarchical C-FIND: @@ -158,10 +155,12 @@ useful in ServerContext::DecodeDicomInstance() * (2) DicomMap: create a cache to the main DICOM tags index * (3) Check out rapidjson: https://github.com/miloyip/nativejson-benchmark -* (2) Optimize tools/find with ModalitiesInStudies: - https://groups.google.com/g/orthanc-users/c/aN8nqcRd3jw/m/pmc9ylVeAwAJ. - One solution could be: Filter first without ModalitiesInStudies and then - cycle through the responses to filter out with ModalitiesInStudies +* For C-Find results: we could store the computed tags + in metadata on some events like NewSeries + DeletedSeries (same for other computer tags). + OtherTags that could be saved in Metadata as well: + - ModalitiesInStudy + - all computed counters at series/study/patient level + - RequestAttributesSequence (sequence that must be included in all DicomWeb QIDO-RS for series) ======== Database