# HG changeset patch # User Alain Mazy # Date 1647356241 -3600 # Node ID 304514ce84ee6a63010d7046990058b49ac866d7 # Parent e8a2e145c80e79860749273f523c50ad61c7b0bb tools/find + C-Find + list-resources now all using the same code (ExpandResource) to build 'computed tags' diff -r e8a2e145c80e -r 304514ce84ee NEWS --- a/NEWS Tue Mar 15 09:09:52 2022 +0100 +++ b/NEWS Tue Mar 15 15:57:21 2022 +0100 @@ -1,10 +1,6 @@ Pending changes in the mainline =============================== -* BREAKING_CHANGE: (TODO: when CFind uses ExpandResource) - If trying to get SOPClassesInStudy in a study that was saved in an Orthanc version - prior to 1.2.0, you will now get an error. Orthanc logs will show an error message - asking you to POST to /studies/.../reconstruct to rebuild the missing study data. Documentation diff -r e8a2e145c80e -r 304514ce84ee OrthancFramework/Sources/DicomFormat/DicomArray.cpp --- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Tue Mar 15 09:09:52 2022 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp Tue Mar 15 15:57:21 2022 +0100 @@ -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 e8a2e145c80e -r 304514ce84ee OrthancFramework/Sources/DicomFormat/DicomArray.h --- a/OrthancFramework/Sources/DicomFormat/DicomArray.h Tue Mar 15 09:09:52 2022 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomArray.h Tue Mar 15 15:57:21 2022 +0100 @@ -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 e8a2e145c80e -r 304514ce84ee OrthancFramework/Sources/DicomFormat/DicomMap.cpp --- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Tue Mar 15 09:09:52 2022 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Tue Mar 15 15:57:21 2022 +0100 @@ -634,6 +634,85 @@ IsMainDicomTag(tag, ResourceType_Instance)); } + bool DicomMap::IsComputedTag(const DicomTag& tag) + { + return (IsComputedTag(tag, ResourceType_Patient) || + IsComputedTag(tag, ResourceType_Study) || + IsComputedTag(tag, ResourceType_Series) || + IsComputedTag(tag, ResourceType_Instance)); + } + + bool DicomMap::IsComputedTag(const DicomTag& tag, ResourceType 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 false; + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + 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) + { + 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; + } + + const std::set& DicomMap::GetMainDicomTags(ResourceType level) { return DicomMap::MainDicomTagsConfiguration::GetInstance().GetMainDicomTagsByLevel(level); diff -r e8a2e145c80e -r 304514ce84ee OrthancFramework/Sources/DicomFormat/DicomMap.h --- a/OrthancFramework/Sources/DicomFormat/DicomMap.h Tue Mar 15 09:09:52 2022 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h Tue Mar 15 15:57:21 2022 +0100 @@ -137,6 +137,16 @@ static bool IsMainDicomTag(const DicomTag& tag); + 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 const std::set& GetMainDicomTags(ResourceType level); // returns a string uniquely identifying the list of main dicom tags for a level diff -r e8a2e145c80e -r 304514ce84ee OrthancFramework/UnitTestsSources/DicomMapTests.cpp --- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Tue Mar 15 09:09:52 2022 +0100 +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Tue Mar 15 15:57:21 2022 +0100 @@ -556,6 +556,54 @@ } +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); + + 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; diff -r e8a2e145c80e -r 304514ce84ee OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Mar 15 09:09:52 2022 +0100 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Mar 15 15:57:21 2022 +0100 @@ -713,10 +713,11 @@ bool StatelessDatabaseOperations::ExpandResource(ExpandedResource& target, const std::string& publicId, ResourceType level, - const std::set& requestedTags) + const std::set& requestedTags, + ExpandResourceDbFlags expandFlags) { - class Operations : public ReadOnlyOperationsT5< - bool&, ExpandedResource&, const std::string&, ResourceType, const std::set&> + class Operations : public ReadOnlyOperationsT6< + bool&, ExpandedResource&, const std::string&, ResourceType, const std::set&, ExpandResourceDbFlags> { private: @@ -759,163 +760,6 @@ } } - static void ComputeSeriesTags(DicomMap& result, - const std::list& children, - const std::set& requestedTags) - { - if (requestedTags.count(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) > 0) - { - result.SetValue(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, - boost::lexical_cast(children.size()), false); - } - } - - static void ComputeStudyTags(DicomMap& result, - ReadOnlyTransaction& transaction, - const std::string& studyPublicId, - int64_t studyInternalId, - const std::set& requestedTags) - { - std::list seriesInternalIds; - std::list instancesInternalIds; - - transaction.GetChildrenInternalId(seriesInternalIds, studyInternalId); - - if (requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) > 0) - { - result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, - boost::lexical_cast(seriesInternalIds.size()), false); - } - - if (requestedTags.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0) - { - std::set values; - - for (std::list::const_iterator - it = seriesInternalIds.begin(); it != seriesInternalIds.end(); ++it) - { - if (requestedTags.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0) - { - DicomMap tags; - transaction.GetMainDicomTags(tags, *it); - - 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, "\\"); - result.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, modalities, false); - } - - if (requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0 - || requestedTags.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0) - { - for (std::list::const_iterator - it = seriesInternalIds.begin(); it != seriesInternalIds.end(); ++it) - { - std::list seriesInstancesIds; - transaction.GetChildrenInternalId(seriesInstancesIds, *it); - - instancesInternalIds.splice(instancesInternalIds.end(), seriesInstancesIds); - } - - if (requestedTags.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0) - { - result.SetValue(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, - boost::lexical_cast(instancesInternalIds.size()), false); - } - - if (requestedTags.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0) - { - std::set values; - - for (std::list::const_iterator - it = instancesInternalIds.begin(); it != instancesInternalIds.end(); ++it) - { - std::map instanceMetadata; - // Extract the metadata - transaction.GetAllMetadata(instanceMetadata, *it); - - std::string value; - if (!LookupStringMetadata(value, instanceMetadata, MetadataType_Instance_SopClassUid)) - { - throw OrthancException(ErrorCode_InternalError, "Unable to get the SOP Class Uid from an instance of the study " + studyPublicId + " because the instance has been saved with an old version of Orthanc (< 1.2.0). You should POST to /studies/" + studyPublicId + "/reconstruct to avoid this error"); - } - - values.insert(value); - } - - std::string sopClassUids; - Toolbox::JoinStrings(sopClassUids, values, "\\"); - result.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, sopClassUids, false); - } - } - } - - static void ComputePatientTags(DicomMap& result, - ReadOnlyTransaction& transaction, - const std::string& patientPublicId, - int64_t patientInternalId, - const std::set& requestedTags) - { - std::list studiesInternalIds; - std::list seriesInternalIds; - std::list instancesInternalIds; - - 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; - - transaction.GetChildrenInternalId(studiesInternalIds, patientInternalId); - - if (hasNbRelatedStudies) - { - result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, - boost::lexical_cast(studiesInternalIds.size()), false); - } - - if (hasNbRelatedSeries || hasNbRelatedInstances) - { - for (std::list::const_iterator - it = studiesInternalIds.begin(); it != studiesInternalIds.end(); ++it) - { - std::list thisSeriesIds; - transaction.GetChildrenInternalId(thisSeriesIds, *it); - seriesInternalIds.splice(seriesInternalIds.end(), thisSeriesIds); - - if (hasNbRelatedInstances) - { - for (std::list::const_iterator - it2 = seriesInternalIds.begin(); it2 != seriesInternalIds.end(); ++it2) - { - std::list thisInstancesIds; - transaction.GetChildrenInternalId(thisInstancesIds, *it2); - instancesInternalIds.splice(instancesInternalIds.end(), thisInstancesIds); - } - } - } - - if (hasNbRelatedSeries) - { - result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, - boost::lexical_cast(seriesInternalIds.size()), false); - } - - if (hasNbRelatedInstances) - { - result.SetValue(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, - boost::lexical_cast(instancesInternalIds.size()), false); - } - } - } public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, @@ -933,6 +777,7 @@ else { ExpandedResource& target = tuple.get<1>(); + ExpandResourceDbFlags expandFlags = tuple.get<5>(); // Set information about the parent resource (if it exists) if (type == ResourceType_Patient) @@ -952,156 +797,134 @@ target.parentId_ = parent; } - // List the children resources - transaction.GetChildrenPublicId(target.childrenIds_, internalId); - - // Extract the metadata - transaction.GetAllMetadata(target.metadata_, internalId); - - // Set the resource type target.type_ = type; - - switch (type) + target.id_ = tuple.get<2>(); + + if (expandFlags & ExpandResourceDbFlags_IncludeChildren) { - case ResourceType_Patient: - case ResourceType_Study: - break; - - case ResourceType_Series: + // List the children resources + transaction.GetChildrenPublicId(target.childrenIds_, internalId); + } + + if (expandFlags & ExpandResourceDbFlags_IncludeMetadata) + { + // Extract the metadata + transaction.GetAllMetadata(target.metadata_, internalId); + + switch (type) { - 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)) + case ResourceType_Patient: + case ResourceType_Study: + break; + + case ResourceType_Series: { - target.indexInSeries_ = static_cast(i); - } - else - { - target.indexInSeries_ = -1; + 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; } - 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); - - // Record the remaining information - target.id_ = tuple.get<2>(); - - // 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) - { - 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) - { - currentLevel = GetParentResourceType(currentLevel); - - int64_t currentParentId; - if (!transaction.LookupParent(currentParentId, currentInternalId)) + 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; } - 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; + default: + throw OrthancException(ErrorCode_InternalError); } - { // handle the tags that must be rebuilt because they are not saved in DB - if (target.type_ == ResourceType_Study && ( - target.missingRequestedTags_.count(DICOM_TAG_MODALITIES_IN_STUDY) > 0 - || target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) > 0 - || target.missingRequestedTags_.count(DICOM_TAG_SOP_CLASSES_IN_STUDY) > 0 - || target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) > 0 - )) - { - ComputeStudyTags(target.tags_, transaction, target.id_, internalId, requestedTags); - } - - if (target.type_ == ResourceType_Series - && target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) > 0) + // 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); + } + + if (expandFlags & ExpandResourceDbFlags_IncludeMainDicomTags) + { + // 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) + { + 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) { - ComputeSeriesTags(target.tags_, target.childrenIds_, requestedTags); + 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; } - - if (target.type_ == ResourceType_Patient && ( - target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) > 0 - || target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) > 0 - || target.missingRequestedTags_.count(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) > 0 - )) - { - ComputePatientTags(target.tags_, transaction, target.id_, internalId, requestedTags); - } - } - + } } std::string tmp; @@ -1139,7 +962,7 @@ bool found; Operations operations; - operations.Apply(*this, found, target, publicId, level, requestedTags); + operations.Apply(*this, found, target, publicId, level, requestedTags, expandFlags); return found; } diff -r e8a2e145c80e -r 304514ce84ee OrthancServer/Sources/Database/StatelessDatabaseOperations.h --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Mar 15 09:09:52 2022 +0100 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Mar 15 15:57:21 2022 +0100 @@ -64,6 +64,17 @@ 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 { @@ -479,7 +490,8 @@ bool ExpandResource(ExpandedResource& target, const std::string& publicId, ResourceType level, - const std::set& requestedTags); + const std::set& requestedTags, + ExpandResourceDbFlags expandFlags); void GetAllMetadata(std::map& target, const std::string& publicId, diff -r e8a2e145c80e -r 304514ce84ee OrthancServer/Sources/OrthancFindRequestHandler.cpp --- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp Tue Mar 15 09:09:52 2022 +0100 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp Tue Mar 15 15:57:21 2022 +0100 @@ -38,275 +38,27 @@ 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); - 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 +82,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 +97,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 +306,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 e8a2e145c80e -r 304514ce84ee OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Tue Mar 15 09:09:52 2022 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Mar 15 15:57:21 2022 +0100 @@ -2256,6 +2256,203 @@ } + 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); + } + } + bool ServerContext::ExpandResource(Json::Value& target, const std::string& publicId, ResourceType level, @@ -2263,22 +2460,24 @@ const std::set& requestedTags) { std::string unusedInstanceId; - Json::Value unusedValue; + Json::Value* unusedDicomAsJson = NULL; + DicomMap unusedMainDicomTags; - return ExpandResource(target, publicId, unusedInstanceId, unusedValue, level, format, requestedTags); + 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) + 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, instanceId, dicomAsJson, level, requestedTags)) + if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceDbFlags_Default)) { SerializeExpandedResource(target, resource, format, requestedTags); return true; @@ -2289,12 +2488,50 @@ 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) + const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource (if already available) ResourceType level, - const std::set& requestedTags) + const std::set& requestedTags, + ExpandResourceDbFlags expandFlags) { - if (GetIndex().ExpandResource(resource, publicId, level, requestedTags)) + // 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, expandFlags)) { // check the main dicom tags list has not changed since the resource was stored if (resource.mainDicomTagsSignature_ != DicomMap::GetMainDicomTagsSignature(resource.type_)) @@ -2307,11 +2544,12 @@ } // possibly merge missing requested tags from dicom-as-json - if (!resource.missingRequestedTags_.empty()) + if (!resource.missingRequestedTags_.empty() && !DicomMap::HasOnlyComputedTags(resource.missingRequestedTags_)) { std::string instanceId_ = instanceId; - Json::Value dicomAsJson_ = dicomAsJson; - if (dicomAsJson_.isNull()) + DicomMap tagsFromJson; + + if (dicomAsJson == NULL) { if (instanceId_.empty()) { @@ -2331,20 +2569,28 @@ } } - // MORE_TAGS :TODO: log warning (add an option to disable them) - ReadDicomAsJson(dicomAsJson_, instanceId_); + // MORE_TAGS :TODO: log "performance" warning (add an option to disable them) + Json::Value tmpDicomAsJson; + ReadDicomAsJson(tmpDicomAsJson, instanceId_); + tagsFromJson.FromDicomAsJson(tmpDicomAsJson); + } + else + { + tagsFromJson.FromDicomAsJson(*dicomAsJson); } - DicomMap allTags; - allTags.FromDicomAsJson(dicomAsJson_); - - resource.tags_.Merge(allTags); + resource.tags_.Merge(tagsFromJson); } - return true; + // compute the requested tags + ComputeTags(resource, *this, publicId, level, requestedTags); + } + else + { + return false; } - return false; + return true; } } diff -r e8a2e145c80e -r 304514ce84ee OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Tue Mar 15 09:09:52 2022 +0100 +++ b/OrthancServer/Sources/ServerContext.h Tue Mar 15 15:57:21 2022 +0100 @@ -548,18 +548,21 @@ 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 + 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 + const Json::Value* dicomAsJson, // optional: the dicom-as-json for the resource ResourceType level, - const std::set& requestedTags); + const std::set& requestedTags, + ExpandResourceDbFlags expandFlags); }; }