Mercurial > hg > orthanc
changeset 2892:ce310baccda6 db-changes
DicomTagConstraint and DatabaseLookup
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 16 Oct 2018 18:00:05 +0200 |
parents | 497a637366b4 |
children | 1723cbba55c7 |
files | CMakeLists.txt OrthancServer/OrthancFindRequestHandler.cpp OrthancServer/Search/DatabaseLookup.cpp OrthancServer/Search/DatabaseLookup.h OrthancServer/Search/DicomTagConstraint.cpp OrthancServer/Search/DicomTagConstraint.h OrthancServer/ServerEnumerations.h |
diffstat | 7 files changed, 759 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Fri Oct 12 15:18:10 2018 +0200 +++ b/CMakeLists.txt Tue Oct 16 18:00:05 2018 +0200 @@ -70,6 +70,8 @@ OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/QueryRetrieveHandler.cpp + OrthancServer/Search/DatabaseLookup.cpp + OrthancServer/Search/DicomTagConstraint.cpp OrthancServer/Search/HierarchicalMatcher.cpp OrthancServer/Search/IFindConstraint.cpp OrthancServer/Search/ListConstraint.cpp
--- a/OrthancServer/OrthancFindRequestHandler.cpp Fri Oct 12 15:18:10 2018 +0200 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Tue Oct 16 18:00:05 2018 +0200 @@ -613,6 +613,8 @@ if (FilterQueryTag(value, level, tag, manufacturer)) { + // TODO - Move this to "ResourceLookup::AddDicomConstraint()" + ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); // DICOM specifies that searches must be case sensitive, except
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DatabaseLookup.cpp Tue Oct 16 18:00:05 2018 +0200 @@ -0,0 +1,252 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "DatabaseLookup.h" + +#include "../ServerToolbox.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" + +namespace Orthanc +{ + void DatabaseLookup::LoadTags(ResourceType level) + { + const DicomTag* tags = NULL; + size_t size; + + ServerToolbox::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + assert(tags_.find(tags[i]) == tags_.end()); + tags_[tags[i]] = TagInfo(DicomTagType_Identifier, level); + } + + DicomMap::LoadMainDicomTags(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (tags_.find(tags[i]) == tags_.end()) + { + tags_[tags[i]] = TagInfo(DicomTagType_Main, level); + } + } + } + + + DatabaseLookup::DatabaseLookup() + { + LoadTags(ResourceType_Patient); + LoadTags(ResourceType_Study); + LoadTags(ResourceType_Series); + LoadTags(ResourceType_Instance); + } + + + DatabaseLookup::~DatabaseLookup() + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + delete constraints_[i]; + } + } + + + const DicomTagConstraint& DatabaseLookup::GetConstraint(size_t index) const + { + if (index >= constraints_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(constraints_[index] != NULL); + return *constraints_[index]; + } + } + + + void DatabaseLookup::AddConstraint(DicomTagConstraint* constraint) + { + if (constraint == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + constraints_.push_back(constraint); + + std::map<DicomTag, TagInfo>::const_iterator tag = tags_.find(constraint->GetTag()); + + if (tag == tags_.end()) + { + constraint->SetTagInfo(DicomTagType_Generic, ResourceType_Instance); + } + else + { + constraint->SetTagInfo(tag->second.GetType(), tag->second.GetLevel()); + } + } + } + + + bool DatabaseLookup::IsMatch(const DicomMap& value) + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + if (!constraints_[i]->IsMatch(value)) + { + return false; + } + } + + return true; + } + + + void DatabaseLookup::AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitivePN) + { + ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); + + if (vr == ValueRepresentation_Sequence) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + /** + * DICOM specifies that searches must always be case sensitive, + * except for tags with a PN value representation. For PN, Orthanc + * uses the configuration option "CaseSensitivePN" to decide + * whether matching is case-sensitive or case-insensitive. + * + * 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 + **/ + bool caseSensitive = true; + if (vr == ValueRepresentation_PersonName) + { + caseSensitive = caseSensitivePN; + } + + 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); + + if (!lower.empty()) + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive)); + } + + if (!upper.empty()) + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive)); + } + } + else if (dicomQuery.find('\\') != std::string::npos) + { + DicomTag fixedTag(tag); + + if (tag == DICOM_TAG_MODALITIES_IN_STUDY) + { + // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained + // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html + fixedTag = DICOM_TAG_MODALITY; + } + + std::auto_ptr<DicomTagConstraint> constraint + (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive)); + + std::vector<std::string> items; + Toolbox::TokenizeString(items, dicomQuery, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + constraint->AddValue(items[i]); + } + + AddConstraint(constraint.release()); + } + else if (dicomQuery.find('*') != std::string::npos || + dicomQuery.find('?') != std::string::npos) + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive)); + } + else + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_Equal, dicomQuery, caseSensitive)); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DatabaseLookup.h Tue Oct 16 18:00:05 2018 +0200 @@ -0,0 +1,104 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "DicomTagConstraint.h" + +namespace Orthanc +{ + class DatabaseLookup : public boost::noncopyable + { + private: + class TagInfo + { + private: + DicomTagType type_; + ResourceType level_; + + public: + TagInfo() : + type_(DicomTagType_Generic), + level_(ResourceType_Instance) + { + } + + TagInfo(DicomTagType type, + ResourceType level) : + type_(type), + level_(level) + { + } + + DicomTagType GetType() const + { + return type_; + } + + ResourceType GetLevel() const + { + return level_; + } + }; + + std::vector<DicomTagConstraint*> constraints_; + std::map<DicomTag, TagInfo> tags_; + + void LoadTags(ResourceType level); + + public: + DatabaseLookup(); + + ~DatabaseLookup(); + + void Reserve(size_t n) + { + constraints_.reserve(n); + } + + size_t GetConstraintsCount() const + { + return constraints_.size(); + } + + const DicomTagConstraint& GetConstraint(size_t index) const; + + void AddConstraint(DicomTagConstraint* constraint); // Takes ownership + + bool IsMatch(const DicomMap& value); + + void AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitivePN); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DicomTagConstraint.cpp Tue Oct 16 18:00:05 2018 +0200 @@ -0,0 +1,274 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "DicomTagConstraint.h" + +#include "../../Core/OrthancException.h" +#include "../../Core/Toolbox.h" + +#include <boost/regex.hpp> + +namespace Orthanc +{ + class DicomTagConstraint::NormalizedString : public boost::noncopyable + { + private: + const std::string& source_; + bool caseSensitive_; + std::string upper_; + + public: + NormalizedString(const std::string& source, + bool caseSensitive) : + source_(source), + caseSensitive_(caseSensitive) + { + if (!caseSensitive_) + { + upper_ = Toolbox::ToUpperCaseWithAccents(source); + } + } + + const std::string& GetValue() const + { + if (caseSensitive_) + { + return source_; + } + else + { + return upper_; + } + } + }; + + + class DicomTagConstraint::RegularExpression : public boost::noncopyable + { + private: + boost::regex regex_; + + public: + RegularExpression(const std::string& source, + bool caseSensitive) + { + NormalizedString normalized(source, caseSensitive); + regex_ = boost::regex(Toolbox::WildcardToRegularExpression(normalized.GetValue())); + } + + const boost::regex& GetValue() const + { + return regex_; + } + }; + + + DicomTagConstraint::DicomTagConstraint(const DicomTag& tag, + ConstraintType type, + const std::string& value, + bool caseSensitive) : + hasTagInfo_(false), + tagType_(DicomTagType_Generic), // Dummy initialization + level_(ResourceType_Patient), // Dummy initialization + tag_(tag), + constraintType_(type), + caseSensitive_(caseSensitive) + { + if (type == ConstraintType_Equal || + type == ConstraintType_SmallerOrEqual || + type == ConstraintType_GreaterOrEqual || + type == ConstraintType_Wildcard) + { + values_.insert(value); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + DicomTagConstraint::DicomTagConstraint(const DicomTag& tag, + ConstraintType type, + bool caseSensitive) : + hasTagInfo_(false), + tagType_(DicomTagType_Generic), // Dummy initialization + level_(ResourceType_Patient), // Dummy initialization + tag_(tag), + constraintType_(type), + caseSensitive_(caseSensitive) + { + if (type != ConstraintType_Wildcard) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void DicomTagConstraint::SetTagInfo(DicomTagType tagType, + ResourceType level) + { + hasTagInfo_ = true; + tagType_ = tagType; + level_ = level; + } + + + DicomTagType DicomTagConstraint::GetTagType() const + { + if (!hasTagInfo_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return tagType_; + } + } + + + const ResourceType DicomTagConstraint::GetLevel() const + { + if (!hasTagInfo_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return level_; + } + } + + + void DicomTagConstraint::AddValue(const std::string& value) + { + if (constraintType_ != ConstraintType_List) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + values_.insert(value); + } + } + + + const std::string& DicomTagConstraint::GetValue() const + { + if (constraintType_ == ConstraintType_List) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else if (values_.size() != 1) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + return *values_.begin(); + } + } + + + bool DicomTagConstraint::IsMatch(const std::string& value) + { + NormalizedString source(value, caseSensitive_); + + switch (constraintType_) + { + case ConstraintType_Equal: + { + NormalizedString reference(GetValue(), caseSensitive_); + return source.GetValue() == reference.GetValue(); + } + + case ConstraintType_SmallerOrEqual: + { + NormalizedString reference(GetValue(), caseSensitive_); + return source.GetValue() <= reference.GetValue(); + } + + case ConstraintType_GreaterOrEqual: + { + NormalizedString reference(GetValue(), caseSensitive_); + return source.GetValue() >= reference.GetValue(); + } + + case ConstraintType_Wildcard: + { + if (regex_.get() == NULL) + { + regex_.reset(new RegularExpression(GetValue(), caseSensitive_)); + } + + return boost::regex_match(source.GetValue(), regex_->GetValue()); + } + + case ConstraintType_List: + { + for (std::set<std::string>::const_iterator + it = values_.begin(); it != values_.end(); ++it) + { + NormalizedString reference(*it, caseSensitive_); + if (source.GetValue() == reference.GetValue()) + { + return true; + } + } + + return false; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + bool DicomTagConstraint::IsMatch(const DicomMap& value) + { + const DicomValue* tmp = value.TestAndGetValue(tag_); + + if (tmp == NULL || + tmp->IsNull() || + tmp->IsBinary()) + { + return false; + } + else + { + return IsMatch(tmp->GetContent()); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DicomTagConstraint.h Tue Oct 16 18:00:05 2018 +0200 @@ -0,0 +1,109 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../ServerEnumerations.h" +#include "../../Core/DicomFormat/DicomMap.h" + +#include <boost/shared_ptr.hpp> + +namespace Orthanc +{ + class DicomTagConstraint : public boost::noncopyable + { + private: + class NormalizedString; + class RegularExpression; + + bool hasTagInfo_; + DicomTagType tagType_; + ResourceType level_; + DicomTag tag_; + ConstraintType constraintType_; + std::set<std::string> values_; + bool caseSensitive_; + + boost::shared_ptr<RegularExpression> regex_; + + public: + DicomTagConstraint(const DicomTag& tag, + ConstraintType type, + const std::string& value, + bool caseSensitive); + + DicomTagConstraint(const DicomTag& tag, + ConstraintType type, + bool caseSensitive); + + bool HasTagInfo() const + { + return hasTagInfo_; + } + + void SetTagInfo(DicomTagType tagType, + ResourceType level); + + DicomTagType GetTagType() const; + + const ResourceType GetLevel() const; + + const DicomTag& GetTag() const + { + return tag_; + } + + ConstraintType GetConstraintType() const + { + return constraintType_; + } + + bool IsCaseSensitive() const + { + return caseSensitive_; + } + + void AddValue(const std::string& value); + + const std::string& GetValue() const; + + const std::set<std::string>& GetValues() const + { + return values_; + } + + bool IsMatch(const std::string& value); + + bool IsMatch(const DicomMap& value); + }; +}
--- a/OrthancServer/ServerEnumerations.h Fri Oct 12 15:18:10 2018 +0200 +++ b/OrthancServer/ServerEnumerations.h Tue Oct 16 18:00:05 2018 +0200 @@ -64,6 +64,22 @@ IdentifierConstraintType_Wildcard /* Case sensitive, "*" or "?" are the only allowed wildcards */ }; + enum DicomTagType + { + DicomTagType_Identifier, // Tag that whose value is stored and indexed in the DB + DicomTagType_Main, // Tag that is stored in the DB (but not indexed) + DicomTagType_Generic // Tag that is only stored in the JSON files + }; + + enum ConstraintType + { + ConstraintType_Equal, + ConstraintType_SmallerOrEqual, + ConstraintType_GreaterOrEqual, + ConstraintType_Wildcard, + ConstraintType_List + }; + /** * WARNING: Do not change the explicit values in the enumerations