# HG changeset patch # User Sebastien Jodogne # Date 1446119120 -3600 # Node ID f7014cca73c7bea98b6344c13782aee1fbc18864 # Parent 8fc1d096aa38e3b1c9a36c29e49bc73d8d2168aa# Parent 2b91363cc1d1d30c7e2638c50050b0591836eac9 integration db-changes->mainline diff -r 8fc1d096aa38 -r f7014cca73c7 CMakeLists.txt --- a/CMakeLists.txt Fri Oct 23 17:04:22 2015 +0200 +++ b/CMakeLists.txt Thu Oct 29 12:45:20 2015 +0100 @@ -150,7 +150,6 @@ OrthancServer/DatabaseWrapper.cpp OrthancServer/DatabaseWrapperBase.cpp OrthancServer/DicomDirWriter.cpp - OrthancServer/DicomFindQuery.cpp OrthancServer/DicomModification.cpp OrthancServer/DicomProtocol/DicomFindAnswers.cpp OrthancServer/DicomProtocol/DicomServer.cpp @@ -179,7 +178,13 @@ OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/ParsedDicomFile.cpp OrthancServer/QueryRetrieveHandler.cpp - OrthancServer/ResourceFinder.cpp + OrthancServer/Search/LookupIdentifierQuery.cpp + OrthancServer/Search/LookupResource.cpp + OrthancServer/Search/SetOfResources.cpp + OrthancServer/Search/ListConstraint.cpp + OrthancServer/Search/RangeConstraint.cpp + OrthancServer/Search/ValueConstraint.cpp + OrthancServer/Search/WildcardConstraint.cpp OrthancServer/ServerContext.cpp OrthancServer/ServerEnumerations.cpp OrthancServer/ServerIndex.cpp diff -r 8fc1d096aa38 -r f7014cca73c7 Core/DicomFormat/DicomMap.cpp --- a/Core/DicomFormat/DicomMap.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/Core/DicomFormat/DicomMap.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -69,7 +69,7 @@ //DicomTag(0x0010, 0x1080), // MilitaryRank DicomTag(0x0008, 0x0021), // SeriesDate DicomTag(0x0008, 0x0031), // SeriesTime - DicomTag(0x0008, 0x0060), // Modality + DICOM_TAG_MODALITY, DicomTag(0x0008, 0x0070), // Manufacturer DicomTag(0x0008, 0x1010), // StationName DICOM_TAG_SERIES_DESCRIPTION, @@ -100,6 +100,36 @@ }; + void DicomMap::LoadMainDicomTags(const DicomTag*& tags, + size_t& size, + ResourceType level) + { + switch (level) + { + case ResourceType_Patient: + tags = patientTags; + size = sizeof(patientTags) / sizeof(DicomTag); + break; + + case ResourceType_Study: + tags = studyTags; + size = sizeof(studyTags) / sizeof(DicomTag); + break; + + case ResourceType_Series: + tags = seriesTags; + size = sizeof(seriesTags) / sizeof(DicomTag); + break; + + case ResourceType_Instance: + tags = instanceTags; + size = sizeof(instanceTags) / sizeof(DicomTag); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } void DicomMap::SetValue(uint16_t group, diff -r 8fc1d096aa38 -r f7014cca73c7 Core/DicomFormat/DicomMap.h --- a/Core/DicomFormat/DicomMap.h Fri Oct 23 17:04:22 2015 +0200 +++ b/Core/DicomFormat/DicomMap.h Thu Oct 29 12:45:20 2015 +0100 @@ -172,5 +172,9 @@ void Print(FILE* fp) const; void GetTags(std::set& tags) const; + + static void LoadMainDicomTags(const DicomTag*& tags, + size_t& size, + ResourceType level); }; } diff -r 8fc1d096aa38 -r f7014cca73c7 Core/DicomFormat/DicomTag.h --- a/Core/DicomFormat/DicomTag.h Fri Oct 23 17:04:22 2015 +0200 +++ b/Core/DicomFormat/DicomTag.h Thu Oct 29 12:45:20 2015 +0100 @@ -109,6 +109,7 @@ static const DicomTag DICOM_TAG_STUDY_DESCRIPTION(0x0008, 0x1030); static const DicomTag DICOM_TAG_SERIES_DESCRIPTION(0x0008, 0x103e); + static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060); // The following is used for "modify/anonymize" operations static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016); diff -r 8fc1d096aa38 -r f7014cca73c7 Core/DicomFormat/DicomValue.cpp --- a/Core/DicomFormat/DicomValue.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/Core/DicomFormat/DicomValue.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -81,10 +81,13 @@ } +#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1 void DicomValue::FormatDataUriScheme(std::string& target, const std::string& mime) const { Toolbox::EncodeBase64(target, GetContent()); target.insert(0, "data:" + mime + ";base64,"); } +#endif + } diff -r 8fc1d096aa38 -r f7014cca73c7 Core/DicomFormat/DicomValue.h --- a/Core/DicomFormat/DicomValue.h Fri Oct 23 17:04:22 2015 +0200 +++ b/Core/DicomFormat/DicomValue.h Thu Oct 29 12:45:20 2015 +0100 @@ -78,6 +78,7 @@ DicomValue* Clone() const; +#if !defined(ORTHANC_ENABLE_BASE64) || ORTHANC_ENABLE_BASE64 == 1 void FormatDataUriScheme(std::string& target, const std::string& mime) const; @@ -85,5 +86,6 @@ { FormatDataUriScheme(target, "application/octet-stream"); } +#endif }; } diff -r 8fc1d096aa38 -r f7014cca73c7 Core/Enumerations.cpp --- a/Core/Enumerations.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/Core/Enumerations.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -148,6 +148,9 @@ case ErrorCode_StorageAreaPlugin: return "Error in the plugin implementing a custom storage area"; + case ErrorCode_EmptyRequest: + return "The request is empty"; + case ErrorCode_SQLiteNotOpened: return "SQLite: The database is not opened"; diff -r 8fc1d096aa38 -r f7014cca73c7 Core/Enumerations.h --- a/Core/Enumerations.h Fri Oct 23 17:04:22 2015 +0200 +++ b/Core/Enumerations.h Thu Oct 29 12:45:20 2015 +0100 @@ -79,6 +79,7 @@ ErrorCode_BadFont = 30 /*!< Badly formatted font file */, ErrorCode_DatabasePlugin = 31 /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */, ErrorCode_StorageAreaPlugin = 32 /*!< Error in the plugin implementing a custom storage area */, + ErrorCode_EmptyRequest = 33 /*!< The request is empty */, 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 8fc1d096aa38 -r f7014cca73c7 Core/FileStorage/StorageAccessor.cpp --- a/Core/FileStorage/StorageAccessor.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/Core/FileStorage/StorageAccessor.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -126,6 +126,20 @@ } + void StorageAccessor::Read(Json::Value& content, + const FileInfo& info) + { + std::string s; + Read(s, info); + + Json::Reader reader; + if (!reader.parse(s, content)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + void StorageAccessor::SetupSender(BufferHttpSender& sender, const FileInfo& info) { diff -r 8fc1d096aa38 -r f7014cca73c7 Core/FileStorage/StorageAccessor.h --- a/Core/FileStorage/StorageAccessor.h Fri Oct 23 17:04:22 2015 +0200 +++ b/Core/FileStorage/StorageAccessor.h Thu Oct 29 12:45:20 2015 +0100 @@ -41,6 +41,7 @@ #include #include #include +#include namespace Orthanc { @@ -75,6 +76,9 @@ void Read(std::string& content, const FileInfo& info); + void Read(Json::Value& content, + const FileInfo& info); + void Remove(const FileInfo& info) { area_.Remove(info.GetUuid(), info.GetContentType()); diff -r 8fc1d096aa38 -r f7014cca73c7 NEWS --- a/NEWS Fri Oct 23 17:04:22 2015 +0200 +++ b/NEWS Thu Oct 29 12:45:20 2015 +0100 @@ -1,6 +1,7 @@ Pending changes in the mainline =============================== +* Full indexation of the patient/study tags to speed up searches and C-Find * Add ".dcm" suffix to files in ZIP archives (cf. URI ".../archive") * "/tools/create-dicom": Support of binary tags encoded using data URI scheme * "/tools/create-dicom": Support of hierarchical structures (creation of sequences) @@ -24,6 +25,7 @@ Maintenance ----------- +* Full refactoring of the searching features * C-Move SCP for studies using AccessionNumber tag * Fix issue 4 (C-Store Association not renegotiated on Specific-to-specific transfer syntax change) * "/system" URI gives information about the plugins used for storage area and DB back-end diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/DatabaseWrapper.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -373,7 +373,7 @@ boost::lexical_cast(GlobalProperty_DatabaseSchemaVersion) + ";"); db_.CommitTransaction(); version_ = 6; - } + } } diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/DatabaseWrapper.h --- a/OrthancServer/DatabaseWrapper.h Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/DatabaseWrapper.h Thu Oct 29 12:45:20 2015 +0100 @@ -249,6 +249,12 @@ return base_.GetResourceCount(resourceType); } + virtual void GetAllInternalIds(std::list& target, + ResourceType resourceType) + { + base_.GetAllInternalIds(target, resourceType); + } + virtual void GetAllPublicIds(std::list& target, ResourceType resourceType) { @@ -315,11 +321,13 @@ return base_.IsExistingResource(internalId); } - virtual void LookupIdentifier(std::list& target, + virtual void LookupIdentifier(std::list& result, + ResourceType level, const DicomTag& tag, + IdentifierConstraintType type, const std::string& value) { - base_.LookupIdentifier(target, tag, value); + base_.LookupIdentifier(result, level, tag, type, value); } virtual void GetAllMetadata(std::map& target, diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/DatabaseWrapperBase.cpp --- a/OrthancServer/DatabaseWrapperBase.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/DatabaseWrapperBase.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -34,6 +34,7 @@ #include "DatabaseWrapperBase.h" #include +#include namespace Orthanc { @@ -532,6 +533,20 @@ return static_cast(s.ColumnInt64(0)); } + void DatabaseWrapperBase::GetAllInternalIds(std::list& target, + ResourceType resourceType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + target.clear(); + while (s.Step()) + { + target.push_back(s.ColumnInt64(0)); + } + } + + void DatabaseWrapperBase::GetAllPublicIds(std::list& target, ResourceType resourceType) { @@ -665,28 +680,51 @@ } + void DatabaseWrapperBase::LookupIdentifier(std::list& target, + ResourceType level, const DicomTag& tag, + IdentifierConstraintType type, const std::string& value) { - assert(tag == DICOM_TAG_PATIENT_ID || - tag == DICOM_TAG_STUDY_INSTANCE_UID || - tag == DICOM_TAG_SERIES_INSTANCE_UID || - tag == DICOM_TAG_SOP_INSTANCE_UID || - tag == DICOM_TAG_ACCESSION_NUMBER); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT id FROM DicomIdentifiers WHERE tagGroup=? AND tagElement=? and value=?"); + static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE " + "d.id = r.internalId AND r.resourceType=? AND " + "d.tagGroup=? AND d.tagElement=? AND "); + + std::auto_ptr s; + + switch (type) + { + case IdentifierConstraintType_GreaterOrEqual: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?")); + break; + + case IdentifierConstraintType_SmallerOrEqual: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?")); + break; - s.BindInt(0, tag.GetGroup()); - s.BindInt(1, tag.GetElement()); - s.BindString(2, value); + case IdentifierConstraintType_Wildcard: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?")); + break; + + case IdentifierConstraintType_Equal: + default: + s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?")); + break; + } + + assert(s.get() != NULL); + + s->BindInt(0, level); + s->BindInt(1, tag.GetGroup()); + s->BindInt(2, tag.GetElement()); + s->BindString(3, value); target.clear(); - while (s.Step()) + while (s->Step()) { - target.push_back(s.ColumnInt64(0)); - } + target.push_back(s->ColumnInt64(0)); + } } } diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/DatabaseWrapperBase.h --- a/OrthancServer/DatabaseWrapperBase.h Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/DatabaseWrapperBase.h Thu Oct 29 12:45:20 2015 +0100 @@ -168,6 +168,9 @@ uint64_t GetTotalUncompressedSize(); + void GetAllInternalIds(std::list& target, + ResourceType resourceType); + void GetAllPublicIds(std::list& target, ResourceType resourceType); @@ -190,8 +193,10 @@ bool IsExistingResource(int64_t internalId); - void LookupIdentifier(std::list& target, + void LookupIdentifier(std::list& result, + ResourceType level, const DicomTag& tag, + IdentifierConstraintType type, const std::string& value); }; } diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/DicomFindQuery.cpp --- a/OrthancServer/DicomFindQuery.cpp Fri Oct 23 17:04:22 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,395 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - - -#include "PrecompiledHeadersServer.h" -#include "DicomFindQuery.h" - -#include "FromDcmtkBridge.h" - -#include - - -namespace Orthanc -{ - class DicomFindQuery::ValueConstraint : public DicomFindQuery::IConstraint - { - private: - bool isCaseSensitive_; - std::string expected_; - - public: - ValueConstraint(const std::string& value, - bool caseSensitive) : - isCaseSensitive_(caseSensitive), - expected_(value) - { - } - - const std::string& GetValue() const - { - return expected_; - } - - virtual bool IsExactConstraint() const - { - return isCaseSensitive_; - } - - virtual bool Apply(const std::string& value) const - { - if (isCaseSensitive_) - { - return expected_ == value; - } - else - { - std::string v, c; - Toolbox::ToLowerCase(v, value); - Toolbox::ToLowerCase(c, expected_); - return v == c; - } - } - }; - - - class DicomFindQuery::ListConstraint : public DicomFindQuery::IConstraint - { - private: - std::set values_; - - public: - ListConstraint(const std::string& values) - { - std::vector items; - Toolbox::TokenizeString(items, values, '\\'); - - for (size_t i = 0; i < items.size(); i++) - { - std::string lower; - Toolbox::ToLowerCase(lower, items[i]); - values_.insert(lower); - } - } - - virtual bool Apply(const std::string& value) const - { - std::string tmp; - Toolbox::ToLowerCase(tmp, value); - return values_.find(tmp) != values_.end(); - } - }; - - - class DicomFindQuery::RangeConstraint : public DicomFindQuery::IConstraint - { - private: - std::string lower_; - std::string upper_; - - public: - RangeConstraint(const std::string& range) - { - size_t separator = range.find('-'); - Toolbox::ToLowerCase(lower_, range.substr(0, separator)); - Toolbox::ToLowerCase(upper_, range.substr(separator + 1)); - } - - virtual bool Apply(const std::string& value) const - { - std::string v; - Toolbox::ToLowerCase(v, value); - - if (lower_.size() == 0 && - upper_.size() == 0) - { - return false; - } - - if (lower_.size() == 0) - { - return v <= upper_; - } - - if (upper_.size() == 0) - { - return v >= lower_; - } - - return (v >= lower_ && v <= upper_); - } - }; - - - class DicomFindQuery::WildcardConstraint : public DicomFindQuery::IConstraint - { - private: - boost::regex pattern_; - - public: - WildcardConstraint(const std::string& wildcard, - bool caseSensitive) - { - std::string re = Toolbox::WildcardToRegularExpression(wildcard); - - if (caseSensitive) - { - pattern_ = boost::regex(re); - } - else - { - pattern_ = boost::regex(re, boost::regex::icase /* case insensitive search */); - } - } - - virtual bool Apply(const std::string& value) const - { - return boost::regex_match(value, pattern_); - } - }; - - - void DicomFindQuery::PrepareMainDicomTags(ResourceType level) - { - std::set tags; - DicomMap::GetMainDicomTags(tags, level); - - for (std::set::const_iterator - it = tags.begin(); it != tags.end(); ++it) - { - mainDicomTags_[*it] = level; - } - } - - - DicomFindQuery::DicomFindQuery() : - level_(ResourceType_Patient), - filterJson_(false) - { - PrepareMainDicomTags(ResourceType_Patient); - PrepareMainDicomTags(ResourceType_Study); - PrepareMainDicomTags(ResourceType_Series); - PrepareMainDicomTags(ResourceType_Instance); - } - - - DicomFindQuery::~DicomFindQuery() - { - for (Constraints::iterator it = constraints_.begin(); - it != constraints_.end(); ++it) - { - delete it->second; - } - } - - - - - void DicomFindQuery::AssignConstraint(const DicomTag& tag, - IConstraint* constraint) - { - Constraints::iterator it = constraints_.find(tag); - - if (it != constraints_.end()) - { - constraints_.erase(it); - } - - constraints_[tag] = constraint; - - MainDicomTags::const_iterator tmp = mainDicomTags_.find(tag); - if (tmp == mainDicomTags_.end()) - { - // The query depends upon a DICOM tag that is not a main tag - // from the point of view of Orthanc, we need to decode the - // JSON file on the disk. - filterJson_ = true; - } - else - { - filteredLevels_.insert(tmp->second); - } - } - - - void DicomFindQuery::SetConstraint(const DicomTag& tag, - const std::string& constraint, - bool caseSensitivePN) - { - ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); - - bool sensitive = true; - if (vr == ValueRepresentation_PatientName) - { - sensitive = caseSensitivePN; - } - - // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained - // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html - - if ((vr == ValueRepresentation_Date || - vr == ValueRepresentation_DateTime || - vr == ValueRepresentation_Time) && - constraint.find('-') != std::string::npos) - { - /** - * Range matching is only defined for TM, DA and DT value - * representations. This code fixes issues 35 and 37. - * - * Reference: "Range matching is not defined for types of - * Attributes other than dates and times", DICOM PS 3.4, - * C.2.2.2.5 ("Range Matching"). - **/ - AssignConstraint(tag, new RangeConstraint(constraint)); - } - else if (constraint.find('\\') != std::string::npos) - { - AssignConstraint(tag, new ListConstraint(constraint)); - } - else if (constraint.find('*') != std::string::npos || - constraint.find('?') != std::string::npos) - { - AssignConstraint(tag, new WildcardConstraint(constraint, sensitive)); - } - else - { - /** - * Case-insensitive match for PN value representation (Patient - * Name). Case-senstive match for all the other value - * representations. - * - * Reference: DICOM PS 3.4 - * - C.2.2.2.1 ("Single Value Matching") - * - C.2.2.2.4 ("Wild Card Matching") - * http://medical.nema.org/Dicom/2011/11_04pu.pdf - * - * "Except for Attributes with a PN Value Representation, only - * entities with values which match exactly the value specified in the - * request shall match. This matching is case-sensitive, i.e., - * sensitive to the exact encoding of the key attribute value in - * character sets where a letter may have multiple encodings (e.g., - * based on its case, its position in a word, or whether it is - * accented) - * - * For Attributes with a PN Value Representation (e.g., Patient Name - * (0010,0010)), an application may perform literal matching that is - * either case-sensitive, or that is insensitive to some or all - * aspects of case, position, accent, or other character encoding - * variants." - * - * (0008,0018) UI SOPInstanceUID => Case-sensitive - * (0008,0050) SH AccessionNumber => Case-sensitive - * (0010,0020) LO PatientID => Case-sensitive - * (0020,000D) UI StudyInstanceUID => Case-sensitive - * (0020,000E) UI SeriesInstanceUID => Case-sensitive - **/ - - AssignConstraint(tag, new ValueConstraint(constraint, sensitive)); - } - } - - - bool DicomFindQuery::RestrictIdentifier(std::string& value, - DicomTag identifier) const - { - Constraints::const_iterator it = constraints_.find(identifier); - if (it == constraints_.end() || - !it->second->IsExactConstraint()) - { - return false; - } - else - { - value = dynamic_cast(it->second)->GetValue(); - return true; - } - } - - bool DicomFindQuery::HasMainDicomTagsFilter(ResourceType level) const - { - return filteredLevels_.find(level) != filteredLevels_.end(); - } - - bool DicomFindQuery::FilterMainDicomTags(const std::string& resourceId, - ResourceType level, - const DicomMap& mainTags) const - { - std::set tags; - mainTags.GetTags(tags); - - for (std::set::const_iterator - it = tags.begin(); it != tags.end(); ++it) - { - const DicomValue& value = mainTags.GetValue(*it); - if (value.IsBinary() || value.IsNull()) - { - return false; - } - - Constraints::const_iterator constraint = constraints_.find(*it); - if (constraint != constraints_.end() && - !constraint->second->Apply(value.GetContent())) - { - return false; - } - } - - return true; - } - - bool DicomFindQuery::HasInstanceFilter() const - { - return filterJson_; - } - - bool DicomFindQuery::FilterInstance(const std::string& instanceId, - const Json::Value& content) const - { - for (Constraints::const_iterator it = constraints_.begin(); - it != constraints_.end(); ++it) - { - std::string tag = it->first.Format(); - std::string value; - if (content.isMember(tag)) - { - value = content.get(tag, Json::arrayValue).get("Value", "").asString(); - } - - if (!it->second->Apply(value)) - { - return false; - } - } - - return true; - } -} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/DicomFindQuery.h --- a/OrthancServer/DicomFindQuery.h Fri Oct 23 17:04:22 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "ResourceFinder.h" - -namespace Orthanc -{ - class DicomFindQuery : public ResourceFinder::IQuery - { - private: - class IConstraint : public boost::noncopyable - { - public: - virtual ~IConstraint() - { - } - - virtual bool IsExactConstraint() const - { - return false; - } - - virtual bool Apply(const std::string& value) const = 0; - }; - - - class ValueConstraint; - class RangeConstraint; - class ListConstraint; - class WildcardConstraint; - - typedef std::map Constraints; - typedef std::map MainDicomTags; - - MainDicomTags mainDicomTags_; - ResourceType level_; - bool filterJson_; - Constraints constraints_; - std::set filteredLevels_; - - void AssignConstraint(const DicomTag& tag, - IConstraint* constraint); - - void PrepareMainDicomTags(ResourceType level); - - - public: - DicomFindQuery(); - - virtual ~DicomFindQuery(); - - void SetLevel(ResourceType level) - { - level_ = level; - } - - virtual ResourceType GetLevel() const - { - return level_; - } - - void SetConstraint(const DicomTag& tag, - const std::string& constraint, - bool caseSensitivePN); - - virtual bool RestrictIdentifier(std::string& value, - DicomTag identifier) const; - - virtual bool HasMainDicomTagsFilter(ResourceType level) const; - - virtual bool FilterMainDicomTags(const std::string& resourceId, - ResourceType level, - const DicomMap& mainTags) const; - - virtual bool HasInstanceFilter() const; - - virtual bool FilterInstance(const std::string& instanceId, - const Json::Value& content) const; - }; -} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/IDatabaseWrapper.h --- a/OrthancServer/IDatabaseWrapper.h Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/IDatabaseWrapper.h Thu Oct 29 12:45:20 2015 +0100 @@ -83,6 +83,9 @@ virtual void GetAllMetadata(std::map& target, int64_t id) = 0; + virtual void GetAllInternalIds(std::list& target, + ResourceType resourceType) = 0; + virtual void GetAllPublicIds(std::list& target, ResourceType resourceType) = 0; @@ -146,8 +149,10 @@ virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property) = 0; - virtual void LookupIdentifier(std::list& target, + virtual void LookupIdentifier(std::list& result, + ResourceType level, const DicomTag& tag, + IdentifierConstraintType type, const std::string& value) = 0; virtual bool LookupMetadata(std::string& target, diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/OrthancFindRequestHandler.cpp --- a/OrthancServer/OrthancFindRequestHandler.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -33,14 +33,12 @@ #include "PrecompiledHeadersServer.h" #include "OrthancFindRequestHandler.h" +#include "../Core/DicomFormat/DicomArray.h" #include "../Core/Logging.h" -#include "../Core/DicomFormat/DicomArray.h" -#include "ServerToolbox.h" +#include "FromDcmtkBridge.h" #include "OrthancInitialization.h" -#include "FromDcmtkBridge.h" - -#include "ResourceFinder.h" -#include "DicomFindQuery.h" +#include "Search/LookupResource.h" +#include "ServerToolbox.h" #include @@ -90,130 +88,6 @@ } - namespace - { - class CFindQuery : public DicomFindQuery - { - private: - DicomFindAnswers& answers_; - ServerIndex& index_; - const DicomArray& query_; - bool hasModalitiesInStudy_; - std::set modalitiesInStudy_; - - public: - CFindQuery(DicomFindAnswers& answers, - ServerIndex& index, - const DicomArray& query) : - answers_(answers), - index_(index), - query_(query), - hasModalitiesInStudy_(false) - { - } - - void SetModalitiesInStudy(const std::string& value) - { - hasModalitiesInStudy_ = true; - - std::vector tmp; - Toolbox::TokenizeString(tmp, value, '\\'); - - for (size_t i = 0; i < tmp.size(); i++) - { - modalitiesInStudy_.insert(tmp[i]); - } - } - - virtual bool HasMainDicomTagsFilter(ResourceType level) const - { - if (DicomFindQuery::HasMainDicomTagsFilter(level)) - { - return true; - } - - return (level == ResourceType_Study && - hasModalitiesInStudy_); - } - - virtual bool FilterMainDicomTags(const std::string& resourceId, - ResourceType level, - const DicomMap& mainTags) const - { - if (!DicomFindQuery::FilterMainDicomTags(resourceId, level, mainTags)) - { - return false; - } - - if (level != ResourceType_Study || - !hasModalitiesInStudy_) - { - return true; - } - - try - { - // We are considering a single study, and the - // "MODALITIES_IN_STUDY" tag is set in the C-Find. Check - // whether one of its child series matches one of the - // modalities. - - Json::Value study; - if (index_.LookupResource(study, resourceId, ResourceType_Study)) - { - // Loop over the series of the considered study. - for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++) - { - Json::Value series; - if (index_.LookupResource(series, study["Series"][j].asString(), ResourceType_Series)) - { - // Get the modality of this series - if (series["MainDicomTags"].isMember("Modality")) - { - std::string modality = series["MainDicomTags"]["Modality"].asString(); - if (modalitiesInStudy_.find(modality) != modalitiesInStudy_.end()) - { - // This series of the considered study matches one - // of the required modalities. Take the study into - // consideration for future filtering. - return true; - } - } - } - } - } - } - catch (OrthancException&) - { - // This resource has probably been deleted during the find request - } - - return false; - } - - virtual bool HasInstanceFilter() const - { - return true; - } - - virtual bool FilterInstance(const std::string& instanceId, - const Json::Value& content) const - { - bool ok = DicomFindQuery::FilterInstance(instanceId, content); - - if (ok) - { - // Add this resource to the answers - AddAnswer(answers_, content, query_); - } - - return ok; - } - }; - } - - - bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, const DicomMap& input, const std::string& remoteIp, @@ -276,9 +150,8 @@ * Build up the query object. **/ - CFindQuery findQuery(answers, context_.GetIndex(), query); - findQuery.SetLevel(level); - + LookupResource finder(level); + for (size_t i = 0; i < query.GetSize(); i++) { const DicomTag tag = query.GetElement(i).GetTag(); @@ -297,14 +170,17 @@ continue; } - if (tag == DICOM_TAG_MODALITIES_IN_STUDY) + ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); + + // DICOM specifies that searches must be case sensitive, except + // for tags with a PN value representation + bool sensitive = true; + if (vr == ValueRepresentation_PatientName) { - findQuery.SetModalitiesInStudy(value); + sensitive = caseSensitivePN; } - else - { - findQuery.SetConstraint(tag, value, caseSensitivePN); - } + + finder.AddDicomConstraint(tag, value, sensitive); } @@ -312,28 +188,35 @@ * Run the query. **/ - ResourceFinder finder(context_); + size_t maxResults = (level == ResourceType_Instance) ? maxInstances_ : maxResults_; + + std::vector resources, instances; + context_.GetIndex().FindCandidates(resources, instances, finder); - switch (level) + assert(resources.size() == instances.size()); + bool finished = true; + + for (size_t i = 0; i < instances.size(); i++) { - case ResourceType_Patient: - case ResourceType_Study: - case ResourceType_Series: - finder.SetMaxResults(maxResults_); - break; - - case ResourceType_Instance: - finder.SetMaxResults(maxInstances_); - break; - - default: - throw OrthancException(ErrorCode_InternalError); + Json::Value dicom; + context_.ReadJson(dicom, instances[i]); + + if (finder.IsMatch(dicom)) + { + if (maxResults != 0 && + answers.GetSize() >= maxResults) + { + finished = false; + break; + } + else + { + AddAnswer(answers, dicom, query); + } + } } - std::list tmp; - bool finished = finder.Apply(tmp, findQuery); - - LOG(INFO) << "Number of matching resources: " << tmp.size(); + LOG(INFO) << "Number of matching resources: " << answers.GetSize(); return finished; } diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/OrthancMoveRequestHandler.cpp --- a/OrthancServer/OrthancMoveRequestHandler.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -148,7 +148,7 @@ const std::string& content = value.GetContent(); std::list ids; - context_.GetIndex().LookupIdentifier(ids, tag, content, level); + context_.GetIndex().LookupIdentifierExact(ids, level, tag, content); if (ids.size() != 1) { diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -36,8 +36,6 @@ #include "../../Core/Logging.h" #include "../ServerToolbox.h" #include "../FromDcmtkBridge.h" -#include "../ResourceFinder.h" -#include "../DicomFindQuery.h" #include "../ServerContext.h" #include "../SliceOrdering.h" @@ -892,7 +890,7 @@ ResourceType level) { std::list tmp; - index.LookupIdentifier(tmp, tag, value, level); + index.LookupIdentifierExact(tmp, level, tag, value); for (std::list::const_iterator it = tmp.begin(); it != tmp.end(); ++it) @@ -944,7 +942,8 @@ request.isMember("Query") && request["Level"].type() == Json::stringValue && request["Query"].type() == Json::objectValue && - (!request.isMember("CaseSensitive") || request["CaseSensitive"].type() == Json::booleanValue)) + (!request.isMember("CaseSensitive") || request["CaseSensitive"].type() == Json::booleanValue) && + (!request.isMember("Limit") || request["Limit"].type() == Json::intValue)) { bool expand = false; if (request.isMember("Expand")) @@ -958,10 +957,19 @@ caseSensitive = request["CaseSensitive"].asBool(); } + size_t limit = 0; + if (request.isMember("Limit")) + { + limit = request["CaseSensitive"].asInt(); + if (limit < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + std::string level = request["Level"].asString(); - DicomFindQuery query; - query.SetLevel(StringToResourceType(level.c_str())); + LookupResource query(StringToResourceType(level.c_str())); Json::Value::Members members = request["Query"].getMemberNames(); for (size_t i = 0; i < members.size(); i++) @@ -971,14 +979,13 @@ throw OrthancException(ErrorCode_BadRequest); } - query.SetConstraint(FromDcmtkBridge::ParseTag(members[i]), - request["Query"][members[i]].asString(), - caseSensitive); + query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), + request["Query"][members[i]].asString(), + caseSensitive); } std::list resources; - ResourceFinder finder(context); - finder.Apply(resources, query); + context.Apply(resources, query, limit); AnswerListOfResources(call.GetOutput(), context.GetIndex(), resources, query.GetLevel(), expand); } else diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/ResourceFinder.cpp --- a/OrthancServer/ResourceFinder.cpp Fri Oct 23 17:04:22 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,409 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "PrecompiledHeadersServer.h" -#include "ResourceFinder.h" - -#include "../Core/Logging.h" -#include "FromDcmtkBridge.h" -#include "ServerContext.h" - -namespace Orthanc -{ - class ResourceFinder::CandidateResources - { - private: - typedef std::map Query; - - ResourceFinder& finder_; - ServerIndex& index_; - ResourceType level_; - bool isFilterApplied_; - std::set filtered_; - - - static void ListToSet(std::set& target, - const std::list& source) - { - for (std::list::const_iterator - it = source.begin(); it != source.end(); ++it) - { - target.insert(*it); - } - } - - - public: - CandidateResources(ResourceFinder& finder) : - finder_(finder), - index_(finder.context_.GetIndex()), - level_(ResourceType_Patient), - isFilterApplied_(false) - { - } - - ResourceType GetLevel() const - { - return level_; - } - - void GoDown() - { - assert(level_ != ResourceType_Instance); - - if (isFilterApplied_) - { - std::set tmp = filtered_; - - filtered_.clear(); - - for (std::set::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - std::list children; - try - { - index_.GetChildren(children, *it); - ListToSet(filtered_, children); - } - catch (OrthancException&) - { - // The resource was removed in the meantime - } - } - } - - switch (level_) - { - case ResourceType_Patient: - level_ = ResourceType_Study; - break; - - case ResourceType_Study: - level_ = ResourceType_Series; - break; - - case ResourceType_Series: - level_ = ResourceType_Instance; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - - void Flatten(std::list& resources) const - { - resources.clear(); - - if (isFilterApplied_) - { - for (std::set::const_iterator - it = filtered_.begin(); it != filtered_.end(); ++it) - { - resources.push_back(*it); - } - } - else - { - index_.GetAllUuids(resources, level_); - } - } - - - void RestrictIdentifier(const IQuery& query, - const DicomTag& tag) - { - assert((level_ == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) || - (level_ == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) || - (level_ == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) || - (level_ == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) || - (level_ == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID)); - - std::string value; - if (!query.RestrictIdentifier(value, tag)) - { - return; - } - - LOG(INFO) << "Lookup for identifier tag " - << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")"; - - std::list resources; - index_.LookupIdentifier(resources, tag, value, level_); - - if (isFilterApplied_) - { - std::set s; - ListToSet(s, resources); - - std::set tmp = filtered_; - filtered_.clear(); - - for (std::set::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) - { - if (s.find(*it) != s.end()) - { - filtered_.insert(*it); - } - } - } - else - { - assert(filtered_.empty()); - isFilterApplied_ = true; - ListToSet(filtered_, resources); - } - } - - - void RestrictMainDicomTags(const IQuery& query, - bool filterPatientTagsAtStudyLevel) - { - if (filterPatientTagsAtStudyLevel && - level_ == ResourceType_Patient) - { - return; - } - - bool hasTagsAtThisLevel = query.HasMainDicomTagsFilter(level_); - bool hasTagsAtPatientLevel = (filterPatientTagsAtStudyLevel && - level_ == ResourceType_Study && - query.HasMainDicomTagsFilter(ResourceType_Patient)); - - if (!hasTagsAtThisLevel && !hasTagsAtPatientLevel) - { - return; - } - - std::list resources; - Flatten(resources); - - isFilterApplied_ = true; - filtered_.clear(); - - for (std::list::const_iterator - it = resources.begin(); it != resources.end(); ++it) - { - DicomMap mainTags; - - if (hasTagsAtThisLevel && - (!index_.GetMainDicomTags(mainTags, *it, level_, level_) || - !query.FilterMainDicomTags(*it, level_, mainTags))) - { - continue; - } - - if (hasTagsAtPatientLevel && - (!index_.GetMainDicomTags(mainTags, *it, ResourceType_Study, ResourceType_Patient) || - !query.FilterMainDicomTags(*it, ResourceType_Patient, mainTags))) - { - continue; - } - - filtered_.insert(*it); - } - } - }; - - - ResourceFinder::ResourceFinder(ServerContext& context) : - context_(context), - maxResults_(0) - { - } - - - void ResourceFinder::ApplyAtLevel(CandidateResources& candidates, - const IQuery& query, - ResourceType level) - { - if (level != ResourceType_Patient) - { - candidates.GoDown(); - } - - switch (level) - { - case ResourceType_Patient: - { - candidates.RestrictIdentifier(query, DICOM_TAG_PATIENT_ID); - break; - } - - case ResourceType_Study: - { - candidates.RestrictIdentifier(query, DICOM_TAG_STUDY_INSTANCE_UID); - candidates.RestrictIdentifier(query, DICOM_TAG_ACCESSION_NUMBER); - break; - } - - case ResourceType_Series: - { - candidates.RestrictIdentifier(query, DICOM_TAG_SERIES_INSTANCE_UID); - break; - } - - case ResourceType_Instance: - { - candidates.RestrictIdentifier(query, DICOM_TAG_SOP_INSTANCE_UID); - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - if (query.GetLevel() == ResourceType_Patient) - { - candidates.RestrictMainDicomTags(query, false); - } - else - { - candidates.RestrictMainDicomTags(query, true); - } - } - - - - static bool LookupOneInstance(std::string& result, - ServerIndex& index, - const std::string& id, - ResourceType type) - { - if (type == ResourceType_Instance) - { - result = id; - return true; - } - - std::string childId; - - { - std::list children; - index.GetChildInstances(children, id); - - if (children.empty()) - { - return false; - } - - childId = children.front(); - } - - return LookupOneInstance(result, index, childId, GetChildResourceType(type)); - } - - - bool ResourceFinder::Apply(std::list& result, - const IQuery& query) - { - CandidateResources candidates(*this); - - ApplyAtLevel(candidates, query, ResourceType_Patient); - - const ResourceType level = query.GetLevel(); - - if (level == ResourceType_Study || - level == ResourceType_Series || - level == ResourceType_Instance) - { - ApplyAtLevel(candidates, query, ResourceType_Study); - } - - if (level == ResourceType_Series || - level == ResourceType_Instance) - { - ApplyAtLevel(candidates, query, ResourceType_Series); - } - - if (level == ResourceType_Instance) - { - ApplyAtLevel(candidates, query, ResourceType_Instance); - } - - if (!query.HasInstanceFilter()) - { - candidates.Flatten(result); - - if (maxResults_ != 0 && - result.size() >= maxResults_) - { - result.resize(maxResults_); - return false; - } - else - { - return true; - } - } - else - { - std::list tmp; - candidates.Flatten(tmp); - - result.clear(); - for (std::list::const_iterator - resource = tmp.begin(); resource != tmp.end(); ++resource) - { - try - { - std::string instance; - if (LookupOneInstance(instance, context_.GetIndex(), *resource, level)) - { - Json::Value content; - context_.ReadJson(content, instance); - if (query.FilterInstance(*resource, content)) - { - result.push_back(*resource); - - if (maxResults_ != 0 && - result.size() >= maxResults_) - { - // Too many results, stop before recording this new match - return false; - } - } - } - } - catch (OrthancException&) - { - // This resource has been deleted since the search was started - } - } - } - - return true; // All the matching resources have been returned - } -} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/ResourceFinder.h --- a/OrthancServer/ResourceFinder.h Fri Oct 23 17:04:22 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,101 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "ServerIndex.h" - -#include - -namespace Orthanc -{ - class ResourceFinder : public boost::noncopyable - { - public: - class IQuery : public boost::noncopyable - { - public: - virtual ~IQuery() - { - } - - virtual ResourceType GetLevel() const = 0; - - virtual bool RestrictIdentifier(std::string& value, - DicomTag identifier) const = 0; - - virtual bool HasMainDicomTagsFilter(ResourceType level) const = 0; - - virtual bool FilterMainDicomTags(const std::string& resourceId, - ResourceType level, - const DicomMap& mainTags) const = 0; - - virtual bool HasInstanceFilter() const = 0; - - virtual bool FilterInstance(const std::string& instanceId, - const Json::Value& content) const = 0; - }; - - - private: - typedef std::map Identifiers; - - class CandidateResources; - - ServerContext& context_; - size_t maxResults_; - - void ApplyAtLevel(CandidateResources& candidates, - const IQuery& query, - ResourceType level); - - public: - ResourceFinder(ServerContext& context); - - void SetMaxResults(size_t value) - { - maxResults_ = value; - } - - size_t GetMaxResults() const - { - return maxResults_; - } - - // Returns "true" iff. all the matching resources have been - // returned. Will be "false" if the results were truncated by - // "SetMaxResults()". - bool Apply(std::list& result, - const IQuery& query); - }; - -} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/IFindConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/IFindConstraint.h Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "LookupIdentifierQuery.h" + +namespace Orthanc +{ + class IFindConstraint : public boost::noncopyable + { + public: + virtual ~IFindConstraint() + { + } + + virtual IFindConstraint* Clone() const = 0; + + virtual void Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const = 0; + + virtual bool Match(const std::string& value) const = 0; + }; +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/ListConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/ListConstraint.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "ListConstraint.h" + + +namespace Orthanc +{ + void ListConstraint::AddAllowedValue(const std::string& value) + { + if (isCaseSensitive_) + { + allowedValues_.insert(value); + } + else + { + std::string s = value; + Toolbox::ToUpperCase(s); + allowedValues_.insert(s); + } + } + + + void ListConstraint::Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const + { + LookupIdentifierQuery::Disjunction& target = lookup.AddDisjunction(); + + for (std::set::const_iterator + it = allowedValues_.begin(); it != allowedValues_.end(); ++it) + { + target.Add(tag, IdentifierConstraintType_Equal, *it); + } + } + + + bool ListConstraint::Match(const std::string& value) const + { + std::string v = value; + + if (!isCaseSensitive_) + { + Toolbox::ToUpperCase(v); + } + + return allowedValues_.find(v) != allowedValues_.end(); + } +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/ListConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/ListConstraint.h Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,71 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IFindConstraint.h" + +#include + +namespace Orthanc +{ + class ListConstraint : public IFindConstraint + { + private: + std::set allowedValues_; + bool isCaseSensitive_; + + ListConstraint(const ListConstraint& other) : + allowedValues_(other.allowedValues_), + isCaseSensitive_(other.isCaseSensitive_) + { + } + + public: + ListConstraint(bool isCaseSensitive) : + isCaseSensitive_(isCaseSensitive) + { + } + + void AddAllowedValue(const std::string& value); + + virtual IFindConstraint* Clone() const + { + return new ListConstraint(*this); + } + + virtual void Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const; + + virtual bool Match(const std::string& value) const; + }; +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/LookupIdentifierQuery.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/LookupIdentifierQuery.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,265 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "LookupIdentifierQuery.h" + +#include "../../Core/OrthancException.h" +#include "SetOfResources.h" +#include "../FromDcmtkBridge.h" + +#include + + + +namespace Orthanc +{ + static const DicomTag patientIdentifiers[] = + { + DICOM_TAG_PATIENT_ID, + DICOM_TAG_PATIENT_NAME, + DICOM_TAG_PATIENT_BIRTH_DATE + }; + + static const DicomTag studyIdentifiers[] = + { + DICOM_TAG_PATIENT_ID, + DICOM_TAG_PATIENT_NAME, + DICOM_TAG_PATIENT_BIRTH_DATE, + DICOM_TAG_STUDY_INSTANCE_UID, + DICOM_TAG_ACCESSION_NUMBER, + DICOM_TAG_STUDY_DESCRIPTION, + DICOM_TAG_STUDY_DATE + }; + + static const DicomTag seriesIdentifiers[] = + { + DICOM_TAG_SERIES_INSTANCE_UID + }; + + static const DicomTag instanceIdentifiers[] = + { + DICOM_TAG_SOP_INSTANCE_UID + }; + + + void LookupIdentifierQuery::LoadIdentifiers(const DicomTag*& tags, + size_t& size, + ResourceType level) + { + switch (level) + { + case ResourceType_Patient: + tags = patientIdentifiers; + size = sizeof(patientIdentifiers) / sizeof(DicomTag); + break; + + case ResourceType_Study: + tags = studyIdentifiers; + size = sizeof(studyIdentifiers) / sizeof(DicomTag); + break; + + case ResourceType_Series: + tags = seriesIdentifiers; + size = sizeof(seriesIdentifiers) / sizeof(DicomTag); + break; + + case ResourceType_Instance: + tags = instanceIdentifiers; + size = sizeof(instanceIdentifiers) / sizeof(DicomTag); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + LookupIdentifierQuery::Disjunction::~Disjunction() + { + for (size_t i = 0; i < disjunction_.size(); i++) + { + delete disjunction_[i]; + } + } + + + void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value) + { + disjunction_.push_back(new Constraint(tag, type, value)); + } + + + LookupIdentifierQuery::~LookupIdentifierQuery() + { + for (Constraints::iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + delete *it; + } + } + + + + bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag, + ResourceType level) + { + const DicomTag* tags; + size_t size; + + LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (tag == tags[i]) + { + return true; + } + } + + return false; + } + + + void LookupIdentifierQuery::AddConstraint(DicomTag tag, + IdentifierConstraintType type, + const std::string& value) + { + assert(IsIdentifier(tag)); + constraints_.push_back(new Disjunction); + constraints_.back()->Add(tag, type, value); + } + + + LookupIdentifierQuery::Disjunction& LookupIdentifierQuery::AddDisjunction() + { + constraints_.push_back(new Disjunction); + return *constraints_.back(); + } + + + std::string LookupIdentifierQuery::NormalizeIdentifier(const std::string& value) + { + std::string s = Toolbox::ConvertToAscii(Toolbox::StripSpaces(value)); + Toolbox::ToUpperCase(s); + return s; + } + + + void LookupIdentifierQuery::StoreIdentifiers(IDatabaseWrapper& database, + int64_t resource, + ResourceType level, + const DicomMap& map) + { + const DicomTag* tags; + size_t size; + + LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + const DicomValue* value = map.TestAndGetValue(tags[i]); + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + std::string s = NormalizeIdentifier(value->GetContent()); + database.SetIdentifierTag(resource, tags[i], s); + } + } + } + + + void LookupIdentifierQuery::Apply(std::list& result, + IDatabaseWrapper& database) + { + SetOfResources resources(database, level_); + Apply(resources, database); + + resources.Flatten(result); + } + + + void LookupIdentifierQuery::Apply(SetOfResources& result, + IDatabaseWrapper& database) + { + for (size_t i = 0; i < GetSize(); i++) + { + std::list a; + + for (size_t j = 0; j < constraints_[i]->GetSize(); j++) + { + const Constraint& constraint = constraints_[i]->GetConstraint(j); + std::list b; + database.LookupIdentifier(b, level_, constraint.GetTag(), constraint.GetType(), constraint.GetValue()); + + a.splice(a.end(), b); + } + + result.Intersect(a); + } + } + + + void LookupIdentifierQuery::Print(std::ostream& s) const + { + s << "Constraint: " << std::endl; + for (Constraints::const_iterator + it = constraints_.begin(); it != constraints_.end(); ++it) + { + if (it == constraints_.begin()) + s << " "; + else + s << "OR "; + + for (size_t j = 0; j < (*it)->GetSize(); j++) + { + const Constraint& c = (*it)->GetConstraint(j); + s << FromDcmtkBridge::GetName(c.GetTag()); + + switch (c.GetType()) + { + case IdentifierConstraintType_Equal: s << " == "; break; + case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break; + case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break; + case IdentifierConstraintType_Wildcard: s << " ~= "; break; + default: + s << " ? "; + } + + s << c.GetValue() << std::endl; + } + } + } +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/LookupIdentifierQuery.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/LookupIdentifierQuery.h Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,183 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../ServerEnumerations.h" +#include "../IDatabaseWrapper.h" + +#include "SetOfResources.h" + +#include +#include + +namespace Orthanc +{ + /** + * Primitive for wildcard matching, as defined in DICOM: + * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4 + * + * "Any occurrence of an "*" or a "?", then "*" shall match any + * sequence of characters (including a zero length value) and "?" + * shall match any single character. This matching is case + * sensitive, except for Attributes with an PN Value + * Representation (e.g., Patient Name (0010,0010))." + * + * Pay attention to the fact that "*" (resp. "?") generally + * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The + * values "%", "_", "\" should in the user request should + * respectively be escaped as "\%", "\_" and "\\". + * + * This matching must be case sensitive: The special case of PN VR + * is taken into consideration by normalizing the query string in + * method "NormalizeIdentifier()". + **/ + + class LookupIdentifierQuery : public boost::noncopyable + { + public: + class Constraint + { + private: + DicomTag tag_; + IdentifierConstraintType type_; + std::string value_; + + public: + Constraint(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value) : + tag_(tag), + type_(type), + value_(NormalizeIdentifier(value)) + { + } + + const DicomTag& GetTag() const + { + return tag_; + } + + IdentifierConstraintType GetType() const + { + return type_; + } + + const std::string& GetValue() const + { + return value_; + } + }; + + + class Disjunction : public boost::noncopyable + { + private: + std::vector disjunction_; + + public: + ~Disjunction(); + + void Add(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value); + + size_t GetSize() const + { + return disjunction_.size(); + } + + const Constraint& GetConstraint(size_t i) const + { + return *disjunction_[i]; + } + }; + + + private: + typedef std::vector Constraints; + + ResourceType level_; + Constraints constraints_; + + static std::string NormalizeIdentifier(const std::string& value); + + public: + LookupIdentifierQuery(ResourceType level) : level_(level) + { + } + + ~LookupIdentifierQuery(); + + bool IsIdentifier(const DicomTag& tag) + { + return IsIdentifier(tag, level_); + } + + void AddConstraint(DicomTag tag, + IdentifierConstraintType type, + const std::string& value); + + Disjunction& AddDisjunction(); + + ResourceType GetLevel() const + { + return level_; + } + + size_t GetSize() const + { + return constraints_.size(); + } + + // The database must be locked + void Apply(std::list& result, + IDatabaseWrapper& database); + + void Apply(SetOfResources& result, + IDatabaseWrapper& database); + + static void LoadIdentifiers(const DicomTag*& tags, + size_t& size, + ResourceType level); + + static bool IsIdentifier(const DicomTag& tag, + ResourceType level); + + static void StoreIdentifiers(IDatabaseWrapper& database, + int64_t resource, + ResourceType level, + const DicomMap& map); + + void Print(std::ostream& s) const; + }; +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/LookupResource.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/LookupResource.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,510 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "LookupResource.h" + +#include "ListConstraint.h" +#include "RangeConstraint.h" +#include "ValueConstraint.h" +#include "WildcardConstraint.h" + +#include "../../Core/OrthancException.h" +#include "../../Core/FileStorage/StorageAccessor.h" +#include "../ServerToolbox.h" +#include "../FromDcmtkBridge.h" + + +namespace Orthanc +{ + LookupResource::Level::Level(ResourceType level) : level_(level) + { + const DicomTag* tags = NULL; + size_t size; + + LookupIdentifierQuery::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + identifiers_.insert(tags[i]); + } + + DicomMap::LoadMainDicomTags(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (identifiers_.find(tags[i]) == identifiers_.end()) + { + mainTags_.insert(tags[i]); + } + } + } + + LookupResource::Level::~Level() + { + for (Constraints::iterator it = mainTagsConstraints_.begin(); + it != mainTagsConstraints_.end(); ++it) + { + delete it->second; + } + + for (Constraints::iterator it = identifiersConstraints_.begin(); + it != identifiersConstraints_.end(); ++it) + { + delete it->second; + } + } + + bool LookupResource::Level::Add(const DicomTag& tag, + std::auto_ptr& constraint) + { + if (identifiers_.find(tag) != identifiers_.end()) + { + if (level_ == ResourceType_Patient) + { + // The filters on the patient level must be cloned to the study level + identifiersConstraints_[tag] = constraint->Clone(); + } + else + { + identifiersConstraints_[tag] = constraint.release(); + } + + return true; + } + else if (mainTags_.find(tag) != mainTags_.end()) + { + if (level_ == ResourceType_Patient) + { + // The filters on the patient level must be cloned to the study level + mainTagsConstraints_[tag] = constraint->Clone(); + } + else + { + mainTagsConstraints_[tag] = constraint.release(); + } + + return true; + } + else + { + return false; + } + } + + + LookupResource::LookupResource(ResourceType level) : level_(level) + { + switch (level) + { + case ResourceType_Patient: + levels_[ResourceType_Patient] = new Level(ResourceType_Patient); + break; + + case ResourceType_Study: + levels_[ResourceType_Study] = new Level(ResourceType_Study); + // Do not add "break" here + + case ResourceType_Series: + levels_[ResourceType_Series] = new Level(ResourceType_Series); + // Do not add "break" here + + case ResourceType_Instance: + levels_[ResourceType_Instance] = new Level(ResourceType_Instance); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + LookupResource::~LookupResource() + { + for (Levels::iterator it = levels_.begin(); + it != levels_.end(); ++it) + { + delete it->second; + } + + for (Constraints::iterator it = unoptimizedConstraints_.begin(); + it != unoptimizedConstraints_.end(); ++it) + { + delete it->second; + } + } + + + + bool LookupResource::AddInternal(ResourceType level, + const DicomTag& tag, + std::auto_ptr& constraint) + { + Levels::iterator it = levels_.find(level); + if (it != levels_.end()) + { + if (it->second->Add(tag, constraint)) + { + return true; + } + } + + return false; + } + + + void LookupResource::Add(const DicomTag& tag, + IFindConstraint* constraint) + { + std::auto_ptr c(constraint); + + if (!AddInternal(ResourceType_Patient, tag, c) && + !AddInternal(ResourceType_Study, tag, c) && + !AddInternal(ResourceType_Series, tag, c) && + !AddInternal(ResourceType_Instance, tag, c)) + { + unoptimizedConstraints_[tag] = c.release(); + } + } + + + static bool Match(const DicomMap& tags, + const DicomTag& tag, + const IFindConstraint& constraint) + { + const DicomValue* value = tags.TestAndGetValue(tag); + + if (value == NULL || + value->IsNull() || + value->IsBinary()) + { + return false; + } + else + { + return constraint.Match(value->GetContent()); + } + } + + + void LookupResource::Level::Apply(SetOfResources& candidates, + IDatabaseWrapper& database) const + { + // First, use the indexed identifiers + LookupIdentifierQuery query(level_); + + for (Constraints::const_iterator it = identifiersConstraints_.begin(); + it != identifiersConstraints_.end(); ++it) + { + it->second->Setup(query, it->first); + } + + query.Apply(candidates, database); + + /*{ + query.Print(std::cout); + std::list source; + candidates.Flatten(source); + printf("=> %d\n", source.size()); + }*/ + + // Secondly, filter using the main DICOM tags + if (!identifiersConstraints_.empty() || + !mainTagsConstraints_.empty()) + { + std::list source; + candidates.Flatten(source); + candidates.Clear(); + + std::list filtered; + for (std::list::const_iterator candidate = source.begin(); + candidate != source.end(); ++candidate) + { + DicomMap tags; + database.GetMainDicomTags(tags, *candidate); + + bool match = true; + + // Re-apply the identifier constraints, as their "Setup" + // method is less restrictive than their "Match" method + for (Constraints::const_iterator it = identifiersConstraints_.begin(); + match && it != identifiersConstraints_.end(); ++it) + { + if (!Match(tags, it->first, *it->second)) + { + match = false; + } + } + + for (Constraints::const_iterator it = mainTagsConstraints_.begin(); + match && it != mainTagsConstraints_.end(); ++it) + { + if (!Match(tags, it->first, *it->second)) + { + match = false; + } + } + + if (match) + { + filtered.push_back(*candidate); + } + } + + candidates.Intersect(filtered); + } + } + + + + bool LookupResource::IsMatch(const Json::Value& dicomAsJson) const + { + for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); + it != unoptimizedConstraints_.end(); ++it) + { + std::string tag = it->first.Format(); + if (dicomAsJson.isMember(tag) && + dicomAsJson[tag]["Type"] == "String") + { + std::string value = dicomAsJson[tag]["Value"].asString(); + if (!it->second->Match(value)) + { + return false; + } + } + else + { + return false; + } + } + + return true; + } + + + void LookupResource::ApplyLevel(SetOfResources& candidates, + ResourceType level, + IDatabaseWrapper& database) const + { + Levels::const_iterator it = levels_.find(level); + if (it != levels_.end()) + { + it->second->Apply(candidates, database); + } + + if (level == ResourceType_Study && + modalitiesInStudy_.get() != NULL) + { + // There is a constraint on the "ModalitiesInStudy" DICOM + // extension. Check out whether one child series has one of the + // allowed modalities + std::list allStudies, matchingStudies; + candidates.Flatten(allStudies); + + for (std::list::const_iterator + study = allStudies.begin(); study != allStudies.end(); ++study) + { + std::list childrenSeries; + database.GetChildrenInternalId(childrenSeries, *study); + + for (std::list::const_iterator + series = childrenSeries.begin(); series != childrenSeries.end(); ++series) + { + DicomMap tags; + database.GetMainDicomTags(tags, *series); + + const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + if (modalitiesInStudy_->Match(value->GetContent())) + { + matchingStudies.push_back(*study); + break; + } + } + } + } + + candidates.Intersect(matchingStudies); + } + } + + + void LookupResource::FindCandidates(std::list& result, + IDatabaseWrapper& database) const + { + ResourceType startingLevel; + if (level_ == ResourceType_Patient) + { + startingLevel = ResourceType_Patient; + } + else + { + startingLevel = ResourceType_Study; + } + + SetOfResources candidates(database, startingLevel); + + switch (level_) + { + case ResourceType_Patient: + ApplyLevel(candidates, ResourceType_Patient, database); + break; + + case ResourceType_Study: + ApplyLevel(candidates, ResourceType_Study, database); + break; + + case ResourceType_Series: + ApplyLevel(candidates, ResourceType_Study, database); + candidates.GoDown(); + ApplyLevel(candidates, ResourceType_Series, database); + break; + + case ResourceType_Instance: + ApplyLevel(candidates, ResourceType_Study, database); + candidates.GoDown(); + ApplyLevel(candidates, ResourceType_Series, database); + candidates.GoDown(); + ApplyLevel(candidates, ResourceType_Instance, database); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + candidates.Flatten(result); + } + + + void LookupResource::SetModalitiesInStudy(const std::string& modalities) + { + modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */)); + + std::vector items; + Toolbox::TokenizeString(items, modalities, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + modalitiesInStudy_->AddAllowedValue(items[i]); + } + } + + + void LookupResource::AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive) + { + ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); + + // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained + // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html + if (tag == DICOM_TAG_MODALITIES_IN_STUDY) + { + SetModalitiesInStudy(dicomQuery); + } + else if ((vr == ValueRepresentation_Date || + vr == ValueRepresentation_DateTime || + vr == ValueRepresentation_Time) && + dicomQuery.find('-') != std::string::npos) + { + /** + * Range matching is only defined for TM, DA and DT value + * representations. This code fixes issues 35 and 37. + * + * Reference: "Range matching is not defined for types of + * Attributes other than dates and times", DICOM PS 3.4, + * C.2.2.2.5 ("Range Matching"). + **/ + size_t separator = dicomQuery.find('-'); + std::string lower = dicomQuery.substr(0, separator); + std::string upper = dicomQuery.substr(separator + 1); + Add(tag, new RangeConstraint(lower, upper, caseSensitive)); + } + else if (dicomQuery.find('\\') != std::string::npos) + { + std::auto_ptr constraint(new ListConstraint(caseSensitive)); + + std::vector items; + Toolbox::TokenizeString(items, dicomQuery, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + constraint->AddAllowedValue(items[i]); + } + + Add(tag, constraint.release()); + } + else if (dicomQuery.find('*') != std::string::npos || + dicomQuery.find('?') != std::string::npos) + { + Add(tag, new WildcardConstraint(dicomQuery, caseSensitive)); + } + else + { + /** + * Case-insensitive match for PN value representation (Patient + * Name). Case-senstive match for all the other value + * representations. + * + * Reference: DICOM PS 3.4 + * - C.2.2.2.1 ("Single Value Matching") + * - C.2.2.2.4 ("Wild Card Matching") + * http://medical.nema.org/Dicom/2011/11_04pu.pdf + * + * "Except for Attributes with a PN Value Representation, only + * entities with values which match exactly the value specified in the + * request shall match. This matching is case-sensitive, i.e., + * sensitive to the exact encoding of the key attribute value in + * character sets where a letter may have multiple encodings (e.g., + * based on its case, its position in a word, or whether it is + * accented) + * + * For Attributes with a PN Value Representation (e.g., Patient Name + * (0010,0010)), an application may perform literal matching that is + * either case-sensitive, or that is insensitive to some or all + * aspects of case, position, accent, or other character encoding + * variants." + * + * (0008,0018) UI SOPInstanceUID => Case-sensitive + * (0008,0050) SH AccessionNumber => Case-sensitive + * (0010,0020) LO PatientID => Case-sensitive + * (0020,000D) UI StudyInstanceUID => Case-sensitive + * (0020,000E) UI SeriesInstanceUID => Case-sensitive + **/ + + Add(tag, new ValueConstraint(dicomQuery, caseSensitive)); + } + } +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/LookupResource.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/LookupResource.h Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,107 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "ListConstraint.h" +#include "SetOfResources.h" + +#include + +namespace Orthanc +{ + class LookupResource : public boost::noncopyable + { + private: + typedef std::map Constraints; + + class Level + { + private: + ResourceType level_; + std::set identifiers_; + std::set mainTags_; + Constraints identifiersConstraints_; + Constraints mainTagsConstraints_; + + public: + Level(ResourceType level); + + ~Level(); + + bool Add(const DicomTag& tag, + std::auto_ptr& constraint); + + void Apply(SetOfResources& candidates, + IDatabaseWrapper& database) const; + }; + + typedef std::map Levels; + + ResourceType level_; + Levels levels_; + Constraints unoptimizedConstraints_; + std::auto_ptr modalitiesInStudy_; + + bool AddInternal(ResourceType level, + const DicomTag& tag, + std::auto_ptr& constraint); + + void ApplyLevel(SetOfResources& candidates, + ResourceType level, + IDatabaseWrapper& database) const; + + public: + LookupResource(ResourceType level); + + ~LookupResource(); + + ResourceType GetLevel() const + { + return level_; + } + + void SetModalitiesInStudy(const std::string& modalities); + + void Add(const DicomTag& tag, + IFindConstraint* constraint); // Takes ownership + + void AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive); + + void FindCandidates(std::list& result, + IDatabaseWrapper& database) const; + + bool IsMatch(const Json::Value& dicomAsJson) const; + }; +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/RangeConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/RangeConstraint.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,90 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "RangeConstraint.h" + +#include "../../Core/Toolbox.h" + +namespace Orthanc +{ + RangeConstraint::RangeConstraint(const std::string& lower, + const std::string& upper, + bool isCaseSensitive) : + lower_(lower), + upper_(upper), + isCaseSensitive_(isCaseSensitive) + { + if (!isCaseSensitive_) + { + Toolbox::ToUpperCase(lower_); + Toolbox::ToUpperCase(upper_); + } + } + + + void RangeConstraint::Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const + { + lookup.AddConstraint(tag, IdentifierConstraintType_GreaterOrEqual, lower_); + lookup.AddConstraint(tag, IdentifierConstraintType_SmallerOrEqual, upper_); + } + + + bool RangeConstraint::Match(const std::string& value) const + { + std::string v = value; + + if (!isCaseSensitive_) + { + Toolbox::ToUpperCase(v); + } + + if (lower_.size() == 0 && + upper_.size() == 0) + { + return false; + } + + if (lower_.size() == 0) + { + return v <= upper_; + } + + if (upper_.size() == 0) + { + return v >= lower_; + } + + return (v >= lower_ && v <= upper_); + } +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/RangeConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/RangeConstraint.h Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,68 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IFindConstraint.h" + +namespace Orthanc +{ + class RangeConstraint : public IFindConstraint + { + private: + std::string lower_; + std::string upper_; + bool isCaseSensitive_; + + RangeConstraint(const RangeConstraint& other) : + lower_(other.lower_), + upper_(other.upper_), + isCaseSensitive_(other.isCaseSensitive_) + { + } + + public: + RangeConstraint(const std::string& lower, + const std::string& upper, + bool isCaseSensitive); + + virtual IFindConstraint* Clone() const + { + return new RangeConstraint(*this); + } + + virtual void Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const; + + virtual bool Match(const std::string& value) const; + }; +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/SetOfResources.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/SetOfResources.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,156 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "SetOfResources.h" + +#include "../../Core/OrthancException.h" + + +namespace Orthanc +{ + void SetOfResources::Intersect(const std::list& resources) + { + if (resources_.get() == NULL) + { + resources_.reset(new Resources); + + for (std::list::const_iterator + it = resources.begin(); it != resources.end(); ++it) + { + resources_->insert(*it); + } + } + else + { + std::auto_ptr filtered(new Resources); + + for (std::list::const_iterator + it = resources.begin(); it != resources.end(); ++it) + { + if (resources_->find(*it) != resources_->end()) + { + filtered->insert(*it); + } + } + + resources_ = filtered; + } + } + + + void SetOfResources::GoDown() + { + if (level_ == ResourceType_Instance) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (resources_.get() != NULL) + { + std::auto_ptr children(new Resources); + + for (Resources::const_iterator it = resources_->begin(); + it != resources_->end(); ++it) + { + std::list tmp; + database_.GetChildrenInternalId(tmp, *it); + + for (std::list::const_iterator + child = tmp.begin(); child != tmp.end(); ++child) + { + children->insert(*child); + } + } + + resources_ = children; + } + + switch (level_) + { + case ResourceType_Patient: + level_ = ResourceType_Study; + break; + + case ResourceType_Study: + level_ = ResourceType_Series; + break; + + case ResourceType_Series: + level_ = ResourceType_Instance; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + void SetOfResources::Flatten(std::list& result) + { + result.clear(); + + if (resources_.get() == NULL) + { + // All the resources of this level are part of the filter + database_.GetAllPublicIds(result, level_); + } + else + { + for (Resources::const_iterator it = resources_->begin(); + it != resources_->end(); ++it) + { + result.push_back(database_.GetPublicId(*it)); + } + } + } + + + void SetOfResources::Flatten(std::list& result) + { + result.clear(); + + if (resources_.get() == NULL) + { + // All the resources of this level are part of the filter + database_.GetAllInternalIds(result, level_); + } + else + { + for (Resources::const_iterator it = resources_->begin(); + it != resources_->end(); ++it) + { + result.push_back(*it); + } + } + } +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/SetOfResources.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/SetOfResources.h Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../IDatabaseWrapper.h" + +#include +#include +#include + +namespace Orthanc +{ + class SetOfResources : public boost::noncopyable + { + private: + typedef std::set Resources; + + IDatabaseWrapper& database_; + ResourceType level_; + std::auto_ptr resources_; + + public: + SetOfResources(IDatabaseWrapper& database, + ResourceType level) : + database_(database), + level_(level) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + void Intersect(const std::list& resources); + + void GoDown(); + + void Flatten(std::list& result); + + void Flatten(std::list& result); + + void Clear() + { + resources_.reset(NULL); + } + }; +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/ValueConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/ValueConstraint.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,73 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "ValueConstraint.h" + +#include "../../Core/Toolbox.h" + +#include + +namespace Orthanc +{ + ValueConstraint::ValueConstraint(const std::string& value, + bool isCaseSensitive) : + value_(value), + isCaseSensitive_(isCaseSensitive) + { + if (!isCaseSensitive) + { + Toolbox::ToUpperCase(value_); + } + } + + + void ValueConstraint::Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const + { + lookup.AddConstraint(tag, IdentifierConstraintType_Equal, value_); + } + + bool ValueConstraint::Match(const std::string& value) const + { + if (isCaseSensitive_) + { + return value_ == value; + } + else + { + std::string v; + Toolbox::ToUpperCase(v, value); + return value_ == v; + } + } +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/ValueConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/ValueConstraint.h Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,65 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IFindConstraint.h" + +namespace Orthanc +{ + class ValueConstraint : public IFindConstraint + { + private: + std::string value_; + bool isCaseSensitive_; + + ValueConstraint(const ValueConstraint& other) : + value_(other.value_), + isCaseSensitive_(other.isCaseSensitive_) + { + } + + public: + ValueConstraint(const std::string& value, + bool isCaseSensitive); + + virtual IFindConstraint* Clone() const + { + return new ValueConstraint(*this); + } + + virtual void Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const; + + virtual bool Match(const std::string& value) const; + }; +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/WildcardConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/WildcardConstraint.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,81 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "WildcardConstraint.h" + +#include + +namespace Orthanc +{ + struct WildcardConstraint::PImpl + { + boost::regex pattern_; + std::string wildcard_; + }; + + + WildcardConstraint::WildcardConstraint(const WildcardConstraint& other) : + pimpl_(new PImpl(*other.pimpl_)) + { + } + + + WildcardConstraint::WildcardConstraint(const std::string& wildcard, + bool isCaseSensitive) : + pimpl_(new PImpl) + { + pimpl_->wildcard_ = wildcard; + + std::string re = Toolbox::WildcardToRegularExpression(wildcard); + + if (isCaseSensitive) + { + pimpl_->pattern_ = boost::regex(re); + } + else + { + pimpl_->pattern_ = boost::regex(re, boost::regex::icase /* case insensitive search */); + } + } + + bool WildcardConstraint::Match(const std::string& value) const + { + return boost::regex_match(value, pimpl_->pattern_); + } + + void WildcardConstraint::Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const + { + lookup.AddConstraint(tag, IdentifierConstraintType_Wildcard, pimpl_->wildcard_); + } +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/Search/WildcardConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/WildcardConstraint.h Thu Oct 29 12:45:20 2015 +0100 @@ -0,0 +1,63 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IFindConstraint.h" + +#include + +namespace Orthanc +{ + class WildcardConstraint : public IFindConstraint + { + private: + struct PImpl; + boost::shared_ptr pimpl_; + + WildcardConstraint(const WildcardConstraint& other); + + public: + WildcardConstraint(const std::string& wildcard, + bool isCaseSensitive); + + virtual IFindConstraint* Clone() const + { + return new WildcardConstraint(*this); + } + + virtual void Setup(LookupIdentifierQuery& lookup, + const DicomTag& tag) const; + + virtual bool Match(const std::string& value) const; + }; +} diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/ServerContext.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -543,4 +543,39 @@ return false; #endif } + + + bool ServerContext::Apply(std::list& result, + const ::Orthanc::LookupResource& lookup, + size_t maxResults) + { + result.clear(); + + std::vector resources, instances; + GetIndex().FindCandidates(resources, instances, lookup); + + assert(resources.size() == instances.size()); + + for (size_t i = 0; i < instances.size(); i++) + { + Json::Value dicom; + ReadJson(dicom, instances[i]); + + if (lookup.IsMatch(dicom)) + { + if (maxResults != 0 && + result.size() >= maxResults) + { + return false; // too many results + } + else + { + result.push_back(resources[i]); + } + } + } + + return true; // finished + } + } diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/ServerContext.h Thu Oct 29 12:45:20 2015 +0100 @@ -47,6 +47,7 @@ #include "Scheduler/ServerScheduler.h" #include "ServerIndex.h" #include "OrthancHttpHandler.h" +#include "Search/LookupResource.h" #include #include @@ -245,6 +246,10 @@ void Stop(); + bool Apply(std::list& result, + const ::Orthanc::LookupResource& lookup, + size_t maxResults); + /** * Management of the plugins @@ -261,6 +266,5 @@ #endif bool HasPlugins() const; - }; } diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/ServerEnumerations.h Thu Oct 29 12:45:20 2015 +0100 @@ -125,6 +125,14 @@ DicomToJsonFlags_ConvertBinaryToNull) }; + enum IdentifierConstraintType + { + IdentifierConstraintType_Equal, + IdentifierConstraintType_SmallerOrEqual, + IdentifierConstraintType_GreaterOrEqual, + IdentifierConstraintType_Wildcard /* Case sensitive, "*" or "?" are the only allowed wildcards */ + }; + /** * WARNING: Do not change the explicit values in the enumerations diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/ServerIndex.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -45,6 +45,8 @@ #include "../Core/Logging.h" #include "../Core/Uuid.h" #include "../Core/DicomFormat/DicomArray.h" +#include "Search/LookupIdentifierQuery.h" +#include "Search/LookupResource.h" #include "FromDcmtkBridge.h" #include "ServerContext.h" @@ -1895,32 +1897,24 @@ - void ServerIndex::LookupIdentifier(std::list& result, - const DicomTag& tag, - const std::string& value, - ResourceType type) + void ServerIndex::LookupIdentifierExact(std::list& result, + ResourceType level, + const DicomTag& tag, + const std::string& value) { - assert(tag == DICOM_TAG_PATIENT_ID || - tag == DICOM_TAG_STUDY_INSTANCE_UID || - tag == DICOM_TAG_SERIES_INSTANCE_UID || - tag == DICOM_TAG_SOP_INSTANCE_UID || - tag == DICOM_TAG_ACCESSION_NUMBER); + assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) || + (level == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) || + (level == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) || + (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) || + (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID)); result.clear(); boost::mutex::scoped_lock lock(mutex_); - std::list id; - db_.LookupIdentifier(id, tag, value); - - for (std::list::const_iterator - it = id.begin(); it != id.end(); ++it) - { - if (db_.GetResourceType(*it) == type) - { - result.push_back(db_.GetPublicId(*it)); - } - } + LookupIdentifierQuery query(level); + query.AddConstraint(tag, IdentifierConstraintType_Equal, value); + query.Apply(result, db_); } @@ -2119,4 +2113,34 @@ boost::mutex::scoped_lock lock(mutex_); return db_.GetDatabaseVersion(); } + + + void ServerIndex::FindCandidates(std::vector& resources, + std::vector& instances, + const ::Orthanc::LookupResource& lookup) + { + boost::mutex::scoped_lock lock(mutex_); + + std::list tmp; + lookup.FindCandidates(tmp, db_); + + resources.resize(tmp.size()); + instances.resize(tmp.size()); + + size_t pos = 0; + for (std::list::const_iterator + it = tmp.begin(); it != tmp.end(); ++it, pos++) + { + assert(db_.GetResourceType(*it) == lookup.GetLevel()); + + int64_t instance; + if (!Toolbox::FindOneChildInstance(instance, db_, *it, lookup.GetLevel())) + { + throw OrthancException(ErrorCode_InternalError); + } + + resources[pos] = db_.GetPublicId(*it); + instances[pos] = db_.GetPublicId(instance); + } + } } diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/ServerIndex.h Thu Oct 29 12:45:20 2015 +0100 @@ -45,6 +45,7 @@ namespace Orthanc { + class LookupResource; class ServerContext; class ServerIndex : public boost::noncopyable @@ -235,10 +236,10 @@ /* out */ unsigned int& countInstances, const std::string& publicId); - void LookupIdentifier(std::list& result, - const DicomTag& tag, - const std::string& value, - ResourceType type); + void LookupIdentifierExact(std::list& result, + ResourceType level, + const DicomTag& tag, + const std::string& value); StoreStatus AddAttachment(const FileInfo& attachment, const std::string& publicId); @@ -261,5 +262,9 @@ const std::string& publicId); unsigned int GetDatabaseVersion(); + + void FindCandidates(std::vector& resources, + std::vector& instances, + const ::Orthanc::LookupResource& lookup); }; } diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/ServerToolbox.cpp --- a/OrthancServer/ServerToolbox.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/ServerToolbox.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -38,6 +38,7 @@ #include "../Core/Logging.h" #include "../Core/OrthancException.h" #include "ParsedDicomFile.h" +#include "Search/LookupIdentifierQuery.h" #include @@ -193,45 +194,6 @@ } - static void SetIdentifierTagInternal(IDatabaseWrapper& database, - int64_t resource, - const DicomMap& tags, - const DicomTag& tag) - { - const DicomValue* value = tags.TestAndGetValue(tag); - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) - { - std::string s = value->GetContent(); - - if (tag != DICOM_TAG_PATIENT_ID && - tag != DICOM_TAG_STUDY_INSTANCE_UID && - tag != DICOM_TAG_SERIES_INSTANCE_UID && - tag != DICOM_TAG_SOP_INSTANCE_UID && - tag != DICOM_TAG_ACCESSION_NUMBER) - { - s = NormalizeIdentifierTag(s); - } - - database.SetIdentifierTag(resource, tag, s); - } - } - - - static void AttachPatientInformation(IDatabaseWrapper& database, - int64_t resource, - const DicomMap& dicomSummary) - { - DicomMap tags; - dicomSummary.ExtractPatientInformation(tags); - SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_PATIENT_ID); - SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_PATIENT_NAME); - SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_PATIENT_BIRTH_DATE); - SetMainDicomTagsInternal(database, resource, tags); - } - - void SetMainDicomTags(IDatabaseWrapper& database, int64_t resource, ResourceType level, @@ -239,33 +201,30 @@ { // WARNING: The database should be locked with a transaction! + LookupIdentifierQuery::StoreIdentifiers(database, resource, level, dicomSummary); + DicomMap tags; switch (level) { case ResourceType_Patient: - AttachPatientInformation(database, resource, dicomSummary); + dicomSummary.ExtractPatientInformation(tags); break; case ResourceType_Study: // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6) - AttachPatientInformation(database, resource, dicomSummary); + dicomSummary.ExtractPatientInformation(tags); + SetMainDicomTagsInternal(database, resource, tags); dicomSummary.ExtractStudyInformation(tags); - SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_STUDY_INSTANCE_UID); - SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_ACCESSION_NUMBER); - SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_STUDY_DESCRIPTION); - SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_STUDY_DATE); break; case ResourceType_Series: dicomSummary.ExtractSeriesInformation(tags); - SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_SERIES_INSTANCE_UID); break; case ResourceType_Instance: dicomSummary.ExtractInstanceInformation(tags); - SetIdentifierTagInternal(database, resource, tags, DICOM_TAG_SOP_INSTANCE_UID); break; default: @@ -374,13 +333,5 @@ Toolbox::SetMainDicomTags(database, resource, level, dicomSummary); } } - - - std::string NormalizeIdentifierTag(const std::string& value) - { - std::string s = Toolbox::ConvertToAscii(Toolbox::StripSpaces(value)); - Toolbox::ToUpperCase(s); - return s; - } } } diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/ServerToolbox.h --- a/OrthancServer/ServerToolbox.h Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/ServerToolbox.h Thu Oct 29 12:45:20 2015 +0100 @@ -59,7 +59,5 @@ void ReconstructMainDicomTags(IDatabaseWrapper& database, IStorageArea& storageArea, ResourceType level); - - std::string NormalizeIdentifierTag(const std::string& value); } } diff -r 8fc1d096aa38 -r f7014cca73c7 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/OrthancServer/main.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -491,6 +491,7 @@ PrintErrorCode(ErrorCode_BadFont, "Badly formatted font file"); PrintErrorCode(ErrorCode_DatabasePlugin, "The plugin implementing a custom database back-end does not fulfill the proper interface"); PrintErrorCode(ErrorCode_StorageAreaPlugin, "Error in the plugin implementing a custom storage area"); + PrintErrorCode(ErrorCode_EmptyRequest, "The request is empty"); 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 8fc1d096aa38 -r f7014cca73c7 Plugins/Engine/OrthancPluginDatabase.cpp --- a/Plugins/Engine/OrthancPluginDatabase.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/Plugins/Engine/OrthancPluginDatabase.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -274,6 +274,21 @@ } + void OrthancPluginDatabase::GetAllInternalIds(std::list& target, + ResourceType resourceType) + { + if (extensions_.getAllInternalIds == NULL) + { + LOG(ERROR) << "The database plugin does not implement the GetAllInternalIds primitive"; + throw OrthancException(ErrorCode_DatabasePlugin); + } + + ResetAnswers(); + CheckSuccess(extensions_.getAllInternalIds(GetContext(), payload_, Plugins::Convert(resourceType))); + ForwardAnswers(target); + } + + void OrthancPluginDatabase::GetAllPublicIds(std::list& target, ResourceType resourceType) { @@ -594,20 +609,27 @@ } - void OrthancPluginDatabase::LookupIdentifier(std::list& target, + void OrthancPluginDatabase::LookupIdentifier(std::list& result, + ResourceType level, const DicomTag& tag, + IdentifierConstraintType type, const std::string& value) { - ResetAnswers(); + if (extensions_.lookupIdentifier3 == NULL) + { + LOG(ERROR) << "The database plugin does not implement the LookupIdentifier3 primitive"; + throw OrthancException(ErrorCode_DatabasePlugin); + } OrthancPluginDicomTag tmp; tmp.group = tag.GetGroup(); tmp.element = tag.GetElement(); tmp.value = value.c_str(); - CheckSuccess(backend_.lookupIdentifier(GetContext(), payload_, &tmp)); - - ForwardAnswers(target); + ResetAnswers(); + CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level), + &tmp, Plugins::Convert(type))); + ForwardAnswers(result); } diff -r 8fc1d096aa38 -r f7014cca73c7 Plugins/Engine/OrthancPluginDatabase.h --- a/Plugins/Engine/OrthancPluginDatabase.h Fri Oct 23 17:04:22 2015 +0200 +++ b/Plugins/Engine/OrthancPluginDatabase.h Thu Oct 29 12:45:20 2015 +0100 @@ -140,6 +140,9 @@ virtual void GetAllMetadata(std::map& target, int64_t id); + virtual void GetAllInternalIds(std::list& target, + ResourceType resourceType); + virtual void GetAllPublicIds(std::list& target, ResourceType resourceType); @@ -203,8 +206,10 @@ virtual bool LookupGlobalProperty(std::string& target, GlobalProperty property); - virtual void LookupIdentifier(std::list& target, + virtual void LookupIdentifier(std::list& result, + ResourceType level, const DicomTag& tag, + IdentifierConstraintType type, const std::string& value); virtual bool LookupMetadata(std::string& target, diff -r 8fc1d096aa38 -r f7014cca73c7 Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/Plugins/Engine/OrthancPlugins.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -302,6 +302,7 @@ sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) || sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) || sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || + sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) || static_cast(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast(DicomToJsonFlags_IncludeBinary) || static_cast(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast(DicomToJsonFlags_IncludePrivateTags) || static_cast(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast(DicomToJsonFlags_IncludeUnknownTags) || @@ -911,7 +912,7 @@ CheckContextAvailable(); std::list result; - pimpl_->context_->GetIndex().LookupIdentifier(result, tag, p.argument, level); + pimpl_->context_->GetIndex().LookupIdentifierExact(result, level, tag, p.argument); if (result.size() == 1) { diff -r 8fc1d096aa38 -r f7014cca73c7 Plugins/Engine/PluginsEnumerations.cpp --- a/Plugins/Engine/PluginsEnumerations.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/Plugins/Engine/PluginsEnumerations.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -229,6 +229,50 @@ } + OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint) + { + switch (constraint) + { + case IdentifierConstraintType_Equal: + return OrthancPluginIdentifierConstraint_Equal; + + case IdentifierConstraintType_GreaterOrEqual: + return OrthancPluginIdentifierConstraint_GreaterOrEqual; + + case IdentifierConstraintType_SmallerOrEqual: + return OrthancPluginIdentifierConstraint_SmallerOrEqual; + + case IdentifierConstraintType_Wildcard: + return OrthancPluginIdentifierConstraint_Wildcard; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint) + { + switch (constraint) + { + case OrthancPluginIdentifierConstraint_Equal: + return IdentifierConstraintType_Equal; + + case OrthancPluginIdentifierConstraint_GreaterOrEqual: + return IdentifierConstraintType_GreaterOrEqual; + + case OrthancPluginIdentifierConstraint_SmallerOrEqual: + return IdentifierConstraintType_SmallerOrEqual; + + case OrthancPluginIdentifierConstraint_Wildcard: + return IdentifierConstraintType_Wildcard; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + #if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0 DcmEVR Convert(OrthancPluginValueRepresentation vr) { diff -r 8fc1d096aa38 -r f7014cca73c7 Plugins/Engine/PluginsEnumerations.h --- a/Plugins/Engine/PluginsEnumerations.h Fri Oct 23 17:04:22 2015 +0200 +++ b/Plugins/Engine/PluginsEnumerations.h Thu Oct 29 12:45:20 2015 +0100 @@ -61,6 +61,10 @@ DicomToJsonFormat Convert(OrthancPluginDicomToJsonFormat format); + OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint); + + IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint); + #if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0 DcmEVR Convert(OrthancPluginValueRepresentation vr); #endif diff -r 8fc1d096aa38 -r f7014cca73c7 Plugins/Include/orthanc/OrthancCDatabasePlugin.h --- a/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Fri Oct 23 17:04:22 2015 +0200 +++ b/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Thu Oct 29 12:45:20 2015 +0100 @@ -522,7 +522,9 @@ void* payload, int32_t property); - /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + /* Use "OrthancPluginDatabaseExtensions::lookupIdentifier3" + instead of this function as of Orthanc 0.9.5 (db v6), can be set to NULL. + Output: Use OrthancPluginDatabaseAnswerInt64() */ OrthancPluginErrorCode (*lookupIdentifier) ( /* outputs */ OrthancPluginDatabaseContext* context, @@ -661,6 +663,24 @@ /* inputs */ void* payload, int64_t id); + + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + OrthancPluginErrorCode (*getAllInternalIds) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + OrthancPluginResourceType resourceType); + + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ + OrthancPluginErrorCode (*lookupIdentifier3) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + OrthancPluginResourceType resourceType, + const OrthancPluginDicomTag* tag, + OrthancPluginIdentifierConstraint constraint); } OrthancPluginDatabaseExtensions; /*& target, + OrthancPluginResourceType resourceType) = 0; + virtual void GetAllPublicIds(std::list& target, OrthancPluginResourceType resourceType) = 0; @@ -403,15 +406,11 @@ virtual bool LookupGlobalProperty(std::string& target /*out*/, int32_t property) = 0; - /** - * "Identifiers" are necessarily one of the following tags: - * PatientID (0x0010, 0x0020), StudyInstanceUID (0x0020, 0x000d), - * SeriesInstanceUID (0x0020, 0x000e), SOPInstanceUID (0x0008, - * 0x0018) or AccessionNumber (0x0008, 0x0050). - **/ virtual void LookupIdentifier(std::list& target /*out*/, + OrthancPluginResourceType resourceType, uint16_t group, uint16_t element, + OrthancPluginIdentifierConstraint constraint, const char* value) = 0; virtual bool LookupMetadata(std::string& target /*out*/, @@ -683,6 +682,39 @@ } + static OrthancPluginErrorCode GetAllInternalIds(OrthancPluginDatabaseContext* context, + void* payload, + OrthancPluginResourceType resourceType) + { + IDatabaseBackend* backend = reinterpret_cast(payload); + backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); + + try + { + std::list target; + backend->GetAllInternalIds(target, resourceType); + + for (std::list::const_iterator + it = target.begin(); it != target.end(); ++it) + { + OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_, + backend->GetOutput().database_, *it); + } + + return OrthancPluginErrorCode_Success; + } + catch (std::runtime_error& e) + { + LogError(backend, e); + return OrthancPluginErrorCode_DatabasePlugin; + } + catch (DatabaseException& e) + { + return e.GetErrorCode(); + } + } + + static OrthancPluginErrorCode GetAllPublicIds(OrthancPluginDatabaseContext* context, void* payload, OrthancPluginResourceType resourceType) @@ -1295,9 +1327,11 @@ } - static OrthancPluginErrorCode LookupIdentifier(OrthancPluginDatabaseContext* context, - void* payload, - const OrthancPluginDicomTag* tag) + static OrthancPluginErrorCode LookupIdentifier3(OrthancPluginDatabaseContext* context, + void* payload, + OrthancPluginResourceType resourceType, + const OrthancPluginDicomTag* tag, + OrthancPluginIdentifierConstraint constraint) { IDatabaseBackend* backend = reinterpret_cast(payload); backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None); @@ -1305,7 +1339,7 @@ try { std::list target; - backend->LookupIdentifier(target, tag->group, tag->element, tag->value); + backend->LookupIdentifier(target, resourceType, tag->group, tag->element, constraint, tag->value); for (std::list::const_iterator it = target.begin(); it != target.end(); ++it) @@ -1824,7 +1858,7 @@ params.logExportedResource = LogExportedResource; params.lookupAttachment = LookupAttachment; params.lookupGlobalProperty = LookupGlobalProperty; - params.lookupIdentifier = LookupIdentifier; + params.lookupIdentifier = NULL; // Unused starting with Orthanc 0.9.5 (db v6) params.lookupIdentifier2 = NULL; // Unused starting with Orthanc 0.9.5 (db v6) params.lookupMetadata = LookupMetadata; params.lookupParent = LookupParent; @@ -1846,6 +1880,8 @@ extensions.getDatabaseVersion = GetDatabaseVersion; extensions.upgradeDatabase = UpgradeDatabase; extensions.clearMainDicomTags = ClearMainDicomTags; + extensions.getAllInternalIds = GetAllInternalIds; // New in Orthanc 0.9.5 (db v6) + extensions.lookupIdentifier3 = LookupIdentifier3; // New in Orthanc 0.9.5 (db v6) OrthancPluginDatabaseContext* database = OrthancPluginRegisterDatabaseBackendV2(context, ¶ms, &extensions, &backend); if (!context) diff -r 8fc1d096aa38 -r f7014cca73c7 Plugins/Samples/DatabasePlugin/CMakeLists.txt --- a/Plugins/Samples/DatabasePlugin/CMakeLists.txt Fri Oct 23 17:04:22 2015 +0200 +++ b/Plugins/Samples/DatabasePlugin/CMakeLists.txt Thu Oct 29 12:45:20 2015 +0100 @@ -46,6 +46,7 @@ ${ORTHANC_ROOT}/Core/DicomFormat/DicomArray.cpp ${ORTHANC_ROOT}/Core/DicomFormat/DicomMap.cpp ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp + ${ORTHANC_ROOT}/Core/DicomFormat/DicomValue.cpp ${ORTHANC_ROOT}/Core/Enumerations.cpp ${ORTHANC_ROOT}/Core/SQLite/Connection.cpp ${ORTHANC_ROOT}/Core/SQLite/FunctionContext.cpp diff -r 8fc1d096aa38 -r f7014cca73c7 Plugins/Samples/DatabasePlugin/Database.cpp --- a/Plugins/Samples/DatabasePlugin/Database.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/Plugins/Samples/DatabasePlugin/Database.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -340,7 +340,7 @@ { GetOutput().AnswerDicomTag(arr.GetElement(i).GetTag().GetGroup(), arr.GetElement(i).GetTag().GetElement(), - arr.GetElement(i).GetValue().AsString()); + arr.GetElement(i).GetValue().GetContent()); } } diff -r 8fc1d096aa38 -r f7014cca73c7 Plugins/Samples/DatabasePlugin/Database.h --- a/Plugins/Samples/DatabasePlugin/Database.h Fri Oct 23 17:04:22 2015 +0200 +++ b/Plugins/Samples/DatabasePlugin/Database.h Thu Oct 29 12:45:20 2015 +0100 @@ -99,6 +99,12 @@ virtual void DeleteResource(int64_t id); + virtual void GetAllInternalIds(std::list& target, + OrthancPluginResourceType resourceType) + { + base_.GetAllInternalIds(target, Orthanc::Plugins::Convert(resourceType)); + } + virtual void GetAllPublicIds(std::list& target, OrthancPluginResourceType resourceType) { @@ -188,11 +194,15 @@ } virtual void LookupIdentifier(std::list& target /*out*/, + OrthancPluginResourceType level, uint16_t group, uint16_t element, + OrthancPluginIdentifierConstraint constraint, const char* value) { - base_.LookupIdentifier(target, Orthanc::DicomTag(group, element), value); + base_.LookupIdentifier(target, Orthanc::Plugins::Convert(level), + Orthanc::DicomTag(group, element), + Orthanc::Plugins::Convert(constraint), value); } virtual bool LookupMetadata(std::string& target /*out*/, diff -r 8fc1d096aa38 -r f7014cca73c7 Resources/Configuration.json --- a/Resources/Configuration.json Fri Oct 23 17:04:22 2015 +0200 +++ b/Resources/Configuration.json Thu Oct 29 12:45:20 2015 +0100 @@ -258,8 +258,9 @@ // deleted as new requests are issued. "QueryRetrieveSize" : 10, - // When handling a C-Find SCP request, setting this flag to "false" - // will enable case-insensitive match for PN value representation - // (such as PatientName). By default, the search is case-insensitive. + // When handling a C-Find SCP request, setting this flag to "true" + // will enable case-sensitive match for PN value representation + // (such as PatientName). By default, the search is + // case-insensitive, which does not follow the DICOM standard. "CaseSensitivePN" : false } diff -r 8fc1d096aa38 -r f7014cca73c7 Resources/ErrorCodes.json --- a/Resources/ErrorCodes.json Fri Oct 23 17:04:22 2015 +0200 +++ b/Resources/ErrorCodes.json Thu Oct 29 12:45:20 2015 +0100 @@ -185,6 +185,11 @@ "Name": "StorageAreaPlugin", "Description": "Error in the plugin implementing a custom storage area" }, + { + "Code": 33, + "Name": "EmptyRequest", + "Description": "The request is empty" + }, diff -r 8fc1d096aa38 -r f7014cca73c7 UnitTestsSources/ServerIndexTests.cpp --- a/UnitTestsSources/ServerIndexTests.cpp Fri Oct 23 17:04:22 2015 +0200 +++ b/UnitTestsSources/ServerIndexTests.cpp Thu Oct 29 12:45:20 2015 +0100 @@ -39,6 +39,7 @@ #include "../OrthancServer/DatabaseWrapper.h" #include "../OrthancServer/ServerContext.h" #include "../OrthancServer/ServerIndex.h" +#include "../OrthancServer/Search/LookupIdentifierQuery.h" #include #include @@ -245,6 +246,18 @@ throw OrthancException(ErrorCode_InternalError); } } + + + void DoLookup(std::list& result, + ResourceType level, + const DicomTag& tag, + const std::string& value) + { + LookupIdentifierQuery query(level); + query.AddConstraint(tag, IdentifierConstraintType_Equal, value); + query.Apply(result, *index_); + } + }; } @@ -690,36 +703,64 @@ index_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0"); index_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0"); - std::list s; + std::list s; - index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "0"); + DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "0"); ASSERT_EQ(2u, s.size()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end()); + ASSERT_TRUE(std::find(s.begin(), s.end(), "a") != s.end()); + ASSERT_TRUE(std::find(s.begin(), s.end(), "c") != s.end()); - index_->LookupIdentifier(s, DICOM_TAG_SERIES_INSTANCE_UID, "0"); + DoLookup(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, "0"); ASSERT_EQ(1u, s.size()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[3]) != s.end()); + ASSERT_TRUE(std::find(s.begin(), s.end(), "d") != s.end()); - index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1"); + DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "1"); ASSERT_EQ(1u, s.size()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end()); + ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end()); - index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1"); + DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "1"); ASSERT_EQ(1u, s.size()); - ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end()); + ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end()); - index_->LookupIdentifier(s, DICOM_TAG_SERIES_INSTANCE_UID, "1"); + DoLookup(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, "1"); ASSERT_EQ(0u, s.size()); - /*{ - std::list s; - context.GetIndex().LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1.2.250.1.74.20130819132500.29000036381059"); - for (std::list::iterator i = s.begin(); i != s.end(); i++) - { - std::cout << "*** " << *i << std::endl;; - } - }*/ + { + LookupIdentifierQuery query(ResourceType_Study); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "0"); + query.Apply(s, *index_); + ASSERT_EQ(3u, s.size()); + } + + { + LookupIdentifierQuery query(ResourceType_Study); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "0"); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_SmallerOrEqual, "0"); + query.Apply(s, *index_); + ASSERT_EQ(2u, s.size()); + } + + { + LookupIdentifierQuery query(ResourceType_Study); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "1"); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_SmallerOrEqual, "1"); + query.Apply(s, *index_); + ASSERT_EQ(1u, s.size()); + } + + { + LookupIdentifierQuery query(ResourceType_Study); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "1"); + query.Apply(s, *index_); + ASSERT_EQ(1u, s.size()); + } + + { + LookupIdentifierQuery query(ResourceType_Study); + query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "2"); + query.Apply(s, *index_); + ASSERT_EQ(0u, s.size()); + } }