Mercurial > hg > orthanc-databases
changeset 550:9ed9a91bde33 find-refactoring
un-sharing DatabaseConstraint and ISqlLookupFormatter with Orthanc core
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 09 Sep 2024 15:04:48 +0200 |
parents | cd9766f294fa (current diff) e620f36b8e09 (diff) |
children | 7f45f23b10d0 |
files | Framework/Plugins/DatabaseBackendAdapterV4.cpp Framework/Plugins/DatabaseConstraint.cpp Framework/Plugins/DatabaseConstraint.h Framework/Plugins/IDatabaseBackend.h Framework/Plugins/ISqlLookupFormatter.cpp Framework/Plugins/ISqlLookupFormatter.h Framework/Plugins/IndexUnitTests.h Framework/Plugins/MessagesToolbox.cpp Framework/Plugins/MessagesToolbox.h Resources/CMake/DatabasesPluginConfiguration.cmake Resources/Orthanc/Databases/DatabaseConstraint.cpp Resources/Orthanc/Databases/DatabaseConstraint.h Resources/Orthanc/Databases/ISqlLookupFormatter.cpp Resources/Orthanc/Databases/ISqlLookupFormatter.h Resources/SyncOrthancFolder.py |
diffstat | 16 files changed, 1726 insertions(+), 1548 deletions(-) [+] |
line wrap: on
line diff
--- a/Framework/Plugins/DatabaseBackendAdapterV4.cpp Mon Sep 09 12:48:52 2024 +0200 +++ b/Framework/Plugins/DatabaseBackendAdapterV4.cpp Mon Sep 09 15:04:48 2024 +0200 @@ -91,28 +91,6 @@ } - static Orthanc::ResourceType Convert2(Orthanc::DatabasePluginMessages::ResourceType resourceType) - { - switch (resourceType) - { - case Orthanc::DatabasePluginMessages::RESOURCE_PATIENT: - return Orthanc::ResourceType_Patient; - - case Orthanc::DatabasePluginMessages::RESOURCE_STUDY: - return Orthanc::ResourceType_Study; - - case Orthanc::DatabasePluginMessages::RESOURCE_SERIES: - return Orthanc::ResourceType_Series; - - case Orthanc::DatabasePluginMessages::RESOURCE_INSTANCE: - return Orthanc::ResourceType_Instance; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - } - - class Output : public IDatabaseBackendOutput { private: @@ -472,7 +450,7 @@ for (int i = 0; i < request.open().identifier_tags().size(); i++) { const Orthanc::DatabasePluginMessages::Open_Request_IdentifierTag& tag = request.open().identifier_tags(i); - identifierTags.push_back(IdentifierTag(Convert2(tag.level()), + identifierTags.push_back(IdentifierTag(MessagesToolbox::Convert(tag.level()), Orthanc::DicomTag(tag.group(), tag.element()), tag.name())); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Plugins/DatabaseConstraint.cpp Mon Sep 09 15:04:48 2024 +0200 @@ -0,0 +1,389 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +/** + * NB: Until 2024-09-09, this file was synchronized with the following + * folder from the Orthanc main project: + * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Sources/Search/ + **/ + + +#include "DatabaseConstraint.h" + +#include <OrthancException.h> + +#include <boost/lexical_cast.hpp> +#include <cassert> + + +namespace Orthanc +{ + namespace Plugins + { + OrthancPluginResourceType Convert(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return OrthancPluginResourceType_Patient; + + case ResourceType_Study: + return OrthancPluginResourceType_Study; + + case ResourceType_Series: + return OrthancPluginResourceType_Series; + + case ResourceType_Instance: + return OrthancPluginResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + ResourceType Convert(OrthancPluginResourceType type) + { + switch (type) + { + case OrthancPluginResourceType_Patient: + return ResourceType_Patient; + + case OrthancPluginResourceType_Study: + return ResourceType_Study; + + case OrthancPluginResourceType_Series: + return ResourceType_Series; + + case OrthancPluginResourceType_Instance: + return ResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + OrthancPluginConstraintType Convert(ConstraintType constraint) + { + switch (constraint) + { + case ConstraintType_Equal: + return OrthancPluginConstraintType_Equal; + + case ConstraintType_GreaterOrEqual: + return OrthancPluginConstraintType_GreaterOrEqual; + + case ConstraintType_SmallerOrEqual: + return OrthancPluginConstraintType_SmallerOrEqual; + + case ConstraintType_Wildcard: + return OrthancPluginConstraintType_Wildcard; + + case ConstraintType_List: + return OrthancPluginConstraintType_List; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + ConstraintType Convert(OrthancPluginConstraintType constraint) + { + switch (constraint) + { + case OrthancPluginConstraintType_Equal: + return ConstraintType_Equal; + + case OrthancPluginConstraintType_GreaterOrEqual: + return ConstraintType_GreaterOrEqual; + + case OrthancPluginConstraintType_SmallerOrEqual: + return ConstraintType_SmallerOrEqual; + + case OrthancPluginConstraintType_Wildcard: + return ConstraintType_Wildcard; + + case OrthancPluginConstraintType_List: + return ConstraintType_List; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +#endif + } + + DatabaseConstraint::DatabaseConstraint(ResourceType level, + const DicomTag& tag, + bool isIdentifier, + ConstraintType type, + const std::vector<std::string>& values, + bool caseSensitive, + bool mandatory) : + level_(level), + tag_(tag), + isIdentifier_(isIdentifier), + constraintType_(type), + values_(values), + caseSensitive_(caseSensitive), + mandatory_(mandatory) + { + if (type != ConstraintType_List && + values_.size() != 1) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + DatabaseConstraint::DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint) : + level_(Plugins::Convert(constraint.level)), + tag_(constraint.tagGroup, constraint.tagElement), + isIdentifier_(constraint.isIdentifierTag), + constraintType_(Plugins::Convert(constraint.type)), + caseSensitive_(constraint.isCaseSensitive), + mandatory_(constraint.isMandatory) + { + if (constraintType_ != ConstraintType_List && + constraint.valuesCount != 1) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + values_.resize(constraint.valuesCount); + + for (uint32_t i = 0; i < constraint.valuesCount; i++) + { + assert(constraint.values[i] != NULL); + values_[i].assign(constraint.values[i]); + } + } +#endif + + +#if ORTHANC_PLUGINS_HAS_INTEGRATED_FIND == 1 + DatabaseConstraint::DatabaseConstraint(const Orthanc::DatabasePluginMessages::DatabaseConstraint& constraint) : + level_(OrthancDatabases::MessagesToolbox::Convert(constraint.level())), + tag_(constraint.tag_group(), constraint.tag_element()), + isIdentifier_(constraint.is_identifier_tag()), + caseSensitive_(constraint.is_case_sensitive()), + mandatory_(constraint.is_mandatory()) + { + switch (constraint.type()) + { + case Orthanc::DatabasePluginMessages::CONSTRAINT_EQUAL: + constraintType_ = Orthanc::ConstraintType_Equal; + break; + + case Orthanc::DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL: + constraintType_ = Orthanc::ConstraintType_SmallerOrEqual; + break; + + case Orthanc::DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL: + constraintType_ = Orthanc::ConstraintType_GreaterOrEqual; + break; + + case Orthanc::DatabasePluginMessages::CONSTRAINT_WILDCARD: + constraintType_ = Orthanc::ConstraintType_Wildcard; + break; + + case Orthanc::DatabasePluginMessages::CONSTRAINT_LIST: + constraintType_ = Orthanc::ConstraintType_List; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + if (constraintType_ != ConstraintType_List && + constraint.values().size() != 1) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + values_.resize(constraint.values().size()); + + for (int i = 0; i < constraint.values().size(); i++) + { + values_[i] = constraint.values(i); + } + } +#endif + + + const std::string& DatabaseConstraint::GetValue(size_t index) const + { + if (index >= values_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return values_[index]; + } + } + + + const std::string& DatabaseConstraint::GetSingleValue() const + { + if (values_.size() != 1) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return values_[0]; + } + } + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + void DatabaseConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint, + std::vector<const char*>& tmpValues) const + { + memset(&constraint, 0, sizeof(constraint)); + + tmpValues.resize(values_.size()); + + for (size_t i = 0; i < values_.size(); i++) + { + tmpValues[i] = values_[i].c_str(); + } + + constraint.level = Plugins::Convert(level_); + constraint.tagGroup = tag_.GetGroup(); + constraint.tagElement = tag_.GetElement(); + constraint.isIdentifierTag = isIdentifier_; + constraint.isCaseSensitive = caseSensitive_; + constraint.isMandatory = mandatory_; + constraint.type = Plugins::Convert(constraintType_); + constraint.valuesCount = values_.size(); + constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]); + } +#endif + + + void DatabaseConstraints::Clear() + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + delete constraints_[i]; + } + + constraints_.clear(); + } + + + void DatabaseConstraints::AddConstraint(DatabaseConstraint* constraint) + { + if (constraint == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + constraints_.push_back(constraint); + } + } + + + const DatabaseConstraint& DatabaseConstraints::GetConstraint(size_t index) const + { + if (index >= constraints_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(constraints_[index] != NULL); + return *constraints_[index]; + } + } + + + std::string DatabaseConstraints::Format() const + { + std::string s; + + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + const DatabaseConstraint& constraint = *constraints_[i]; + s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) + + ": " + constraint.GetTag().Format(); + + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + s += " == " + constraint.GetSingleValue(); + break; + + case ConstraintType_SmallerOrEqual: + s += " <= " + constraint.GetSingleValue(); + break; + + case ConstraintType_GreaterOrEqual: + s += " >= " + constraint.GetSingleValue(); + break; + + case ConstraintType_Wildcard: + s += " ~~ " + constraint.GetSingleValue(); + break; + + case ConstraintType_List: + { + s += " in [ "; + bool first = true; + for (size_t j = 0; j < constraint.GetValuesCount(); j++) + { + if (first) + { + first = false; + } + else + { + s += ", "; + } + s += constraint.GetValue(j); + } + s += "]"; + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + s += "\n"; + } + + return s; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Plugins/DatabaseConstraint.h Mon Sep 09 15:04:48 2024 +0200 @@ -0,0 +1,171 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +/** + * NB: Until 2024-09-09, this file was synchronized with the following + * folder from the Orthanc main project: + * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Sources/Search/ + **/ + + +#pragma once + +#include "MessagesToolbox.h" + +#include <DicomFormat/DicomMap.h> + +#include <deque> + +namespace Orthanc +{ + enum ConstraintType + { + ConstraintType_Equal, + ConstraintType_SmallerOrEqual, + ConstraintType_GreaterOrEqual, + ConstraintType_Wildcard, + ConstraintType_List + }; + + namespace Plugins + { + OrthancPluginResourceType Convert(ResourceType type); + + ResourceType Convert(OrthancPluginResourceType type); + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + OrthancPluginConstraintType Convert(ConstraintType constraint); +#endif + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + ConstraintType Convert(OrthancPluginConstraintType constraint); +#endif + } + + + class DatabaseConstraint : public boost::noncopyable + { + private: + ResourceType level_; + DicomTag tag_; + bool isIdentifier_; + ConstraintType constraintType_; + std::vector<std::string> values_; + bool caseSensitive_; + bool mandatory_; + + public: + DatabaseConstraint(ResourceType level, + const DicomTag& tag, + bool isIdentifier, + ConstraintType type, + const std::vector<std::string>& values, + bool caseSensitive, + bool mandatory); + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + explicit DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint); +#endif + +#if ORTHANC_PLUGINS_HAS_INTEGRATED_FIND == 1 + explicit DatabaseConstraint(const Orthanc::DatabasePluginMessages::DatabaseConstraint& constraint); +#endif + + ResourceType GetLevel() const + { + return level_; + } + + const DicomTag& GetTag() const + { + return tag_; + } + + bool IsIdentifier() const + { + return isIdentifier_; + } + + ConstraintType GetConstraintType() const + { + return constraintType_; + } + + size_t GetValuesCount() const + { + return values_.size(); + } + + const std::string& GetValue(size_t index) const; + + const std::string& GetSingleValue() const; + + bool IsCaseSensitive() const + { + return caseSensitive_; + } + + bool IsMandatory() const + { + return mandatory_; + } + + bool IsMatch(const DicomMap& dicom) const; + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint, + std::vector<const char*>& tmpValues) const; +#endif + }; + + + class DatabaseConstraints : public boost::noncopyable + { + private: + std::deque<DatabaseConstraint*> constraints_; + + public: + ~DatabaseConstraints() + { + Clear(); + } + + void Clear(); + + void AddConstraint(DatabaseConstraint* constraint); // Takes ownership + + bool IsEmpty() const + { + return constraints_.empty(); + } + + size_t GetSize() const + { + return constraints_.size(); + } + + const DatabaseConstraint& GetConstraint(size_t index) const; + + std::string Format() const; + }; +}
--- a/Framework/Plugins/IDatabaseBackend.h Mon Sep 09 12:48:52 2024 +0200 +++ b/Framework/Plugins/IDatabaseBackend.h Mon Sep 09 15:04:48 2024 +0200 @@ -24,22 +24,14 @@ #pragma once -#include "../../Resources/Orthanc/Databases/ISqlLookupFormatter.h" #include "../Common/DatabaseManager.h" #include "../Common/DatabasesEnumerations.h" #include "IDatabaseBackendOutput.h" +#include "ISqlLookupFormatter.h" #include "IdentifierTag.h" #include <list> -#include <orthanc/OrthancCPlugin.h> - -#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) // Macro introduced in Orthanc 1.3.1 -# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5) -# include <OrthancDatabasePlugin.pb.h> // Include protobuf messages for "Find()" -# endif -#endif - namespace OrthancDatabases { class IDatabaseBackend : public boost::noncopyable
--- a/Framework/Plugins/IDatabaseBackendOutput.h Mon Sep 09 12:48:52 2024 +0200 +++ b/Framework/Plugins/IDatabaseBackendOutput.h Mon Sep 09 15:04:48 2024 +0200 @@ -23,7 +23,7 @@ #pragma once -#include "../../Resources/Orthanc/Databases/DatabaseConstraint.h" +#include "DatabaseConstraint.h" namespace OrthancDatabases {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Plugins/ISqlLookupFormatter.cpp Mon Sep 09 15:04:48 2024 +0200 @@ -0,0 +1,942 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +/** + * NB: Until 2024-09-09, this file was synchronized with the following + * folder from the Orthanc main project: + * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Sources/Search/ + **/ + + +#include "ISqlLookupFormatter.h" + +#include "DatabaseConstraint.h" + +#include <OrthancException.h> +#include <Toolbox.h> + +#include <cassert> +#include <boost/lexical_cast.hpp> +#include <list> + + +namespace Orthanc +{ + static std::string FormatLevel(ResourceType level) + { + switch (level) + { + case ResourceType_Patient: + return "patients"; + + case ResourceType_Study: + return "studies"; + + case ResourceType_Series: + return "series"; + + case ResourceType_Instance: + return "instances"; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + static bool FormatComparison(std::string& target, + ISqlLookupFormatter& formatter, + const DatabaseConstraint& constraint, + size_t index, + bool escapeBrackets) + { + std::string tag = "t" + boost::lexical_cast<std::string>(index); + + std::string comparison; + + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + case ConstraintType_SmallerOrEqual: + case ConstraintType_GreaterOrEqual: + { + std::string op; + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + op = "="; + break; + + case ConstraintType_SmallerOrEqual: + op = "<="; + break; + + case ConstraintType_GreaterOrEqual: + op = ">="; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue()); + + if (constraint.IsCaseSensitive()) + { + comparison = tag + ".value " + op + " " + parameter; + } + else + { + comparison = "lower(" + tag + ".value) " + op + " lower(" + parameter + ")"; + } + + break; + } + + case ConstraintType_List: + { + for (size_t i = 0; i < constraint.GetValuesCount(); i++) + { + if (!comparison.empty()) + { + comparison += ", "; + } + + std::string parameter = formatter.GenerateParameter(constraint.GetValue(i)); + + if (constraint.IsCaseSensitive()) + { + comparison += parameter; + } + else + { + comparison += "lower(" + parameter + ")"; + } + } + + if (constraint.IsCaseSensitive()) + { + comparison = tag + ".value IN (" + comparison + ")"; + } + else + { + comparison = "lower(" + tag + ".value) IN (" + comparison + ")"; + } + + break; + } + + case ConstraintType_Wildcard: + { + const std::string value = constraint.GetSingleValue(); + + if (value == "*") + { + if (!constraint.IsMandatory()) + { + // Universal constraint on an optional tag, ignore it + return false; + } + } + else + { + std::string escaped; + escaped.reserve(value.size()); + + for (size_t i = 0; i < value.size(); i++) + { + if (value[i] == '*') + { + escaped += "%"; + } + else if (value[i] == '?') + { + escaped += "_"; + } + else if (value[i] == '%') + { + escaped += "\\%"; + } + else if (value[i] == '_') + { + escaped += "\\_"; + } + else if (value[i] == '\\') + { + escaped += "\\\\"; + } + else if (escapeBrackets && value[i] == '[') + { + escaped += "\\["; + } + else if (escapeBrackets && value[i] == ']') + { + escaped += "\\]"; + } + else + { + escaped += value[i]; + } + } + + std::string parameter = formatter.GenerateParameter(escaped); + + if (constraint.IsCaseSensitive()) + { + comparison = (tag + ".value LIKE " + parameter + " " + + formatter.FormatWildcardEscape()); + } + else + { + comparison = ("lower(" + tag + ".value) LIKE lower(" + + parameter + ") " + formatter.FormatWildcardEscape()); + } + } + + break; + } + + default: + return false; + } + + if (constraint.IsMandatory()) + { + target = comparison; + } + else if (comparison.empty()) + { + target = tag + ".value IS NULL"; + } + else + { + target = tag + ".value IS NULL OR " + comparison; + } + + return true; + } + + + static void FormatJoin(std::string& target, + const DatabaseConstraint& constraint, + size_t index) + { + std::string tag = "t" + boost::lexical_cast<std::string>(index); + + if (constraint.IsMandatory()) + { + target = " INNER JOIN "; + } + else + { + target = " LEFT JOIN "; + } + + if (constraint.IsIdentifier()) + { + target += "DicomIdentifiers "; + } + else + { + target += "MainDicomTags "; + } + + target += (tag + " ON " + tag + ".id = " + FormatLevel(constraint.GetLevel()) + + ".internalId AND " + tag + ".tagGroup = " + + boost::lexical_cast<std::string>(constraint.GetTag().GetGroup()) + + " AND " + tag + ".tagElement = " + + boost::lexical_cast<std::string>(constraint.GetTag().GetElement())); + } + + + static std::string Join(const std::list<std::string>& values, + const std::string& prefix, + const std::string& separator) + { + if (values.empty()) + { + return ""; + } + else + { + std::string s = prefix; + + bool first = true; + for (std::list<std::string>::const_iterator it = values.begin(); it != values.end(); ++it) + { + if (first) + { + first = false; + } + else + { + s += separator; + } + + s += *it; + } + + return s; + } + } + + static bool FormatComparison2(std::string& target, + ISqlLookupFormatter& formatter, + const DatabaseConstraint& constraint, + bool escapeBrackets) + { + std::string comparison; + std::string tagFilter = ("tagGroup = " + boost::lexical_cast<std::string>(constraint.GetTag().GetGroup()) + + " AND tagElement = " + boost::lexical_cast<std::string>(constraint.GetTag().GetElement())); + + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + case ConstraintType_SmallerOrEqual: + case ConstraintType_GreaterOrEqual: + { + std::string op; + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + op = "="; + break; + + case ConstraintType_SmallerOrEqual: + op = "<="; + break; + + case ConstraintType_GreaterOrEqual: + op = ">="; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue()); + + if (constraint.IsCaseSensitive()) + { + comparison = " AND value " + op + " " + parameter; + } + else + { + comparison = " AND lower(value) " + op + " lower(" + parameter + ")"; + } + + break; + } + + case ConstraintType_List: + { + std::vector<std::string> comparisonValues; + for (size_t i = 0; i < constraint.GetValuesCount(); i++) + { + std::string parameter = formatter.GenerateParameter(constraint.GetValue(i)); + + if (constraint.IsCaseSensitive()) + { + comparisonValues.push_back(parameter); + } + else + { + comparisonValues.push_back("lower(" + parameter + ")"); + } + } + + std::string values; + Toolbox::JoinStrings(values, comparisonValues, ", "); + + if (constraint.IsCaseSensitive()) + { + comparison = " AND value IN (" + values + ")"; + } + else + { + comparison = " AND lower(value) IN (" + values + ")"; + } + + break; + } + + case ConstraintType_Wildcard: + { + const std::string value = constraint.GetSingleValue(); + + if (value == "*") + { + if (!constraint.IsMandatory()) + { + // Universal constraint on an optional tag, ignore it + return false; + } + } + else + { + std::string escaped; + escaped.reserve(value.size()); + + for (size_t i = 0; i < value.size(); i++) + { + if (value[i] == '*') + { + escaped += "%"; + } + else if (value[i] == '?') + { + escaped += "_"; + } + else if (value[i] == '%') + { + escaped += "\\%"; + } + else if (value[i] == '_') + { + escaped += "\\_"; + } + else if (value[i] == '\\') + { + escaped += "\\\\"; + } + else if (escapeBrackets && value[i] == '[') + { + escaped += "\\["; + } + else if (escapeBrackets && value[i] == ']') + { + escaped += "\\]"; + } + else + { + escaped += value[i]; + } + } + + std::string parameter = formatter.GenerateParameter(escaped); + + if (constraint.IsCaseSensitive()) + { + comparison = " AND value LIKE " + parameter + " " + formatter.FormatWildcardEscape(); + } + else + { + comparison = " AND lower(value) LIKE lower(" + parameter + ") " + formatter.FormatWildcardEscape(); + } + } + + break; + } + + default: + return false; + } + + if (constraint.IsMandatory()) + { + target = tagFilter + comparison; + } + else if (comparison.empty()) + { + target = tagFilter + " AND value IS NULL"; + } + else + { + target = tagFilter + " AND value IS NULL OR " + comparison; + } + + return true; + } + + + void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel, + ResourceType& upperLevel, + const ResourceType& queryLevel, + const DatabaseConstraints& lookup) + { + assert(ResourceType_Patient < ResourceType_Study && + ResourceType_Study < ResourceType_Series && + ResourceType_Series < ResourceType_Instance); + + lowerLevel = queryLevel; + upperLevel = queryLevel; + + for (size_t i = 0; i < lookup.GetSize(); i++) + { + ResourceType level = lookup.GetConstraint(i).GetLevel(); + + if (level < upperLevel) + { + upperLevel = level; + } + + if (level > lowerLevel) + { + lowerLevel = level; + } + } + } + + + void ISqlLookupFormatter::Apply(std::string& sql, + ISqlLookupFormatter& formatter, + const DatabaseConstraints& lookup, + ResourceType queryLevel, + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, + size_t limit) + { + ResourceType lowerLevel, upperLevel; + GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup); + + assert(upperLevel <= queryLevel && + queryLevel <= lowerLevel); + + const bool escapeBrackets = formatter.IsEscapeBrackets(); + + std::string joins, comparisons; + + size_t count = 0; + + for (size_t i = 0; i < lookup.GetSize(); i++) + { + const DatabaseConstraint& constraint = lookup.GetConstraint(i); + + std::string comparison; + + if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets)) + { + std::string join; + FormatJoin(join, constraint, count); + joins += join; + + if (!comparison.empty()) + { + comparisons += " AND " + comparison; + } + + count ++; + } + } + + sql = ("SELECT " + + FormatLevel(queryLevel) + ".publicId, " + + FormatLevel(queryLevel) + ".internalId" + + " FROM Resources AS " + FormatLevel(queryLevel)); + + for (int level = queryLevel - 1; level >= upperLevel; level--) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast<ResourceType>(level)) + " ON " + + FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" + + FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId"); + } + + for (int level = queryLevel + 1; level <= lowerLevel; level++) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast<ResourceType>(level)) + " ON " + + FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" + + FormatLevel(static_cast<ResourceType>(level)) + ".parentId"); + } + + std::list<std::string> where; + where.push_back(FormatLevel(queryLevel) + ".resourceType = " + + formatter.FormatResourceType(queryLevel) + comparisons); + + if (!labels.empty()) + { + /** + * "In SQL Server, NOT EXISTS and NOT IN predicates are the best + * way to search for missing values, as long as both columns in + * question are NOT NULL." + * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/ + **/ + + std::list<std::string> formattedLabels; + for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) + { + formattedLabels.push_back(formatter.GenerateParameter(*it)); + } + + std::string condition; + switch (labelsConstraint) + { + case LabelsConstraint_Any: + condition = "> 0"; + break; + + case LabelsConstraint_All: + condition = "= " + boost::lexical_cast<std::string>(labels.size()); + break; + + case LabelsConstraint_None: + condition = "= 0"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + FormatLevel(queryLevel) + + ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition); + } + + sql += joins + Join(where, " WHERE ", " AND "); + + if (limit != 0) + { + sql += " LIMIT " + boost::lexical_cast<std::string>(limit); + } + } + + +#if ORTHANC_PLUGINS_HAS_INTEGRATED_FIND == 1 + static ResourceType DetectLevel(const Orthanc::DatabasePluginMessages::Find_Request& request) + { + // This corresponds to "Orthanc::OrthancIdentifiers()::DetectLevel()" in the Orthanc core + if (!request.orthanc_id_patient().empty() && + request.orthanc_id_study().empty() && + request.orthanc_id_series().empty() && + request.orthanc_id_instance().empty()) + { + return ResourceType_Patient; + } + else if (!request.orthanc_id_study().empty() && + request.orthanc_id_series().empty() && + request.orthanc_id_instance().empty()) + { + return ResourceType_Study; + } + else if (!request.orthanc_id_series().empty() && + request.orthanc_id_instance().empty()) + { + return ResourceType_Series; + } + else if (!request.orthanc_id_instance().empty()) + { + return ResourceType_Instance; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + + void ISqlLookupFormatter::Apply(std::string& sql, + ISqlLookupFormatter& formatter, + const Orthanc::DatabasePluginMessages::Find_Request& request) + { + const bool escapeBrackets = formatter.IsEscapeBrackets(); + ResourceType queryLevel = OrthancDatabases::MessagesToolbox::Convert(request.level()); + const std::string& strQueryLevel = FormatLevel(queryLevel); + + DatabaseConstraints constraints; + + for (int i = 0; i < request.dicom_tag_constraints().size(); i++) + { + constraints.AddConstraint(new DatabaseConstraint(request.dicom_tag_constraints(i))); + } + + ResourceType lowerLevel, upperLevel; + GetLookupLevels(lowerLevel, upperLevel, queryLevel, constraints); + + assert(upperLevel <= queryLevel && + queryLevel <= lowerLevel); + + + sql = ("SELECT " + + strQueryLevel + ".publicId, " + + strQueryLevel + ".internalId" + + " FROM Resources AS " + strQueryLevel); + + + std::string joins, comparisons; + + const bool isOrthancIdentifiersDefined = (!request.orthanc_id_patient().empty() || + !request.orthanc_id_study().empty() || + !request.orthanc_id_series().empty() || + !request.orthanc_id_instance().empty()); + + if (isOrthancIdentifiersDefined && + Orthanc::IsResourceLevelAboveOrEqual(DetectLevel(request), queryLevel)) + { + // single child resource matching, there should not be other constraints (at least for now) + if (request.dicom_tag_constraints().size() != 0 || + request.labels().size() != 0 || + request.has_limits()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + ResourceType topParentLevel = DetectLevel(request); + const std::string& strTopParentLevel = FormatLevel(topParentLevel); + + std::string publicId; + switch (topParentLevel) + { + case ResourceType_Patient: + publicId = request.orthanc_id_patient(); + break; + + case ResourceType_Study: + publicId = request.orthanc_id_study(); + break; + + case ResourceType_Series: + publicId = request.orthanc_id_series(); + break; + + case ResourceType_Instance: + publicId = request.orthanc_id_instance(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (publicId.empty()) + { + throw OrthancException(ErrorCode_InternalError); + } + + comparisons = " AND " + strTopParentLevel + ".publicId = " + formatter.GenerateParameter(publicId); + + for (int level = queryLevel; level > topParentLevel; level--) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast<ResourceType>(level - 1)) + " ON " + + FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" + + FormatLevel(static_cast<ResourceType>(level)) + ".parentId"); + } + } + else + { + size_t count = 0; + + for (size_t i = 0; i < constraints.GetSize(); i++) + { + const DatabaseConstraint& constraint = constraints.GetConstraint(i); + + std::string comparison; + + if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets)) + { + std::string join; + FormatJoin(join, constraint, count); + joins += join; + + if (!comparison.empty()) + { + comparisons += " AND " + comparison; + } + + count ++; + } + } + } + + for (int level = queryLevel - 1; level >= upperLevel; level--) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast<ResourceType>(level)) + " ON " + + FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" + + FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId"); + } + + for (int level = queryLevel + 1; level <= lowerLevel; level++) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast<ResourceType>(level)) + " ON " + + FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" + + FormatLevel(static_cast<ResourceType>(level)) + ".parentId"); + } + + std::list<std::string> where; + where.push_back(strQueryLevel + ".resourceType = " + + formatter.FormatResourceType(queryLevel) + comparisons); + + + if (!request.labels().empty()) + { + /** + * "In SQL Server, NOT EXISTS and NOT IN predicates are the best + * way to search for missing values, as long as both columns in + * question are NOT NULL." + * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/ + **/ + + std::list<std::string> formattedLabels; + for (int i = 0; i < request.labels().size(); i++) + { + formattedLabels.push_back(formatter.GenerateParameter(request.labels(i))); + } + + std::string condition; + switch (request.labels_constraint()) + { + case Orthanc::DatabasePluginMessages::LABELS_CONSTRAINT_ANY: + condition = "> 0"; + break; + + case Orthanc::DatabasePluginMessages::LABELS_CONSTRAINT_ALL: + condition = "= " + boost::lexical_cast<std::string>(request.labels().size()); + break; + + case Orthanc::DatabasePluginMessages::LABELS_CONSTRAINT_NONE: + condition = "= 0"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + strQueryLevel + + ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition); + } + + sql += joins + Join(where, " WHERE ", " AND "); + + if (request.has_limits()) + { + sql += formatter.FormatLimits(request.limits().since(), request.limits().count()); + } + + } +#endif + + + void ISqlLookupFormatter::ApplySingleLevel(std::string& sql, + ISqlLookupFormatter& formatter, + const DatabaseConstraints& lookup, + ResourceType queryLevel, + const std::set<std::string>& labels, + LabelsConstraint labelsConstraint, + size_t limit + ) + { + ResourceType lowerLevel, upperLevel; + GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup); + + assert(upperLevel == queryLevel && + queryLevel == lowerLevel); + + const bool escapeBrackets = formatter.IsEscapeBrackets(); + + std::vector<std::string> mainDicomTagsComparisons, dicomIdentifiersComparisons; + + for (size_t i = 0; i < lookup.GetSize(); i++) + { + const DatabaseConstraint& constraint = lookup.GetConstraint(i); + + std::string comparison; + + if (FormatComparison2(comparison, formatter, constraint, escapeBrackets)) + { + if (!comparison.empty()) + { + if (constraint.IsIdentifier()) + { + dicomIdentifiersComparisons.push_back(comparison); + } + else + { + mainDicomTagsComparisons.push_back(comparison); + } + } + } + } + + sql = ("SELECT publicId, internalId " + "FROM Resources " + "WHERE resourceType = " + formatter.FormatResourceType(queryLevel) + + " "); + + if (dicomIdentifiersComparisons.size() > 0) + { + for (std::vector<std::string>::const_iterator it = dicomIdentifiersComparisons.begin(); it < dicomIdentifiersComparisons.end(); ++it) + { + sql += (" AND internalId IN (SELECT id FROM DicomIdentifiers WHERE " + *it + ") "); + } + } + + if (mainDicomTagsComparisons.size() > 0) + { + for (std::vector<std::string>::const_iterator it = mainDicomTagsComparisons.begin(); it < mainDicomTagsComparisons.end(); ++it) + { + sql += (" AND internalId IN (SELECT id FROM MainDicomTags WHERE " + *it + ") "); + } + } + + if (!labels.empty()) + { + /** + * "In SQL Server, NOT EXISTS and NOT IN predicates are the best + * way to search for missing values, as long as both columns in + * question are NOT NULL." + * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/ + **/ + + std::list<std::string> formattedLabels; + for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) + { + formattedLabels.push_back(formatter.GenerateParameter(*it)); + } + + std::string condition; + std::string inOrNotIn; + switch (labelsConstraint) + { + case LabelsConstraint_Any: + condition = "> 0"; + inOrNotIn = "IN"; + break; + + case LabelsConstraint_All: + condition = "= " + boost::lexical_cast<std::string>(labels.size()); + inOrNotIn = "IN"; + break; + + case LabelsConstraint_None: + condition = "> 0"; + inOrNotIn = "NOT IN"; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + sql += (" AND internalId " + inOrNotIn + " (SELECT id" + " FROM (SELECT id, COUNT(1) AS labelsCount " + "FROM Labels " + "WHERE label IN (" + Join(formattedLabels, "", ", ") + ") GROUP BY id" + ") AS temp " + " WHERE labelsCount " + condition + ")"); + } + + if (limit != 0) + { + sql += " LIMIT " + boost::lexical_cast<std::string>(limit); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Plugins/ISqlLookupFormatter.h Mon Sep 09 15:04:48 2024 +0200 @@ -0,0 +1,99 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +/** + * NB: Until 2024-09-09, this file was synchronized with the following + * folder from the Orthanc main project: + * https://orthanc.uclouvain.be/hg/orthanc/file/default/OrthancServer/Sources/Search/ + **/ + + +#pragma once + +#include "MessagesToolbox.h" + +#include <boost/noncopyable.hpp> +#include <vector> + +namespace Orthanc +{ + class DatabaseConstraints; + class FindRequest; + + enum LabelsConstraint + { + LabelsConstraint_All, + LabelsConstraint_Any, + LabelsConstraint_None + }; + + class ISqlLookupFormatter : public boost::noncopyable + { + public: + virtual ~ISqlLookupFormatter() + { + } + + virtual std::string GenerateParameter(const std::string& value) = 0; + + virtual std::string FormatResourceType(ResourceType level) = 0; + + virtual std::string FormatWildcardEscape() = 0; + + virtual std::string FormatLimits(uint64_t since, uint64_t count) = 0; + + /** + * Whether to escape '[' and ']', which is only needed for + * MSSQL. New in Orthanc 1.10.0, from the following changeset: + * https://orthanc.uclouvain.be/hg/orthanc-databases/rev/389c037387ea + **/ + virtual bool IsEscapeBrackets() const = 0; + + static void GetLookupLevels(ResourceType& lowerLevel, + ResourceType& upperLevel, + const ResourceType& queryLevel, + const DatabaseConstraints& lookup); + + static void Apply(std::string& sql, + ISqlLookupFormatter& formatter, + const DatabaseConstraints& lookup, + ResourceType queryLevel, + const std::set<std::string>& labels, // New in Orthanc 1.12.0 + LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 + size_t limit); + + static void ApplySingleLevel(std::string& sql, + ISqlLookupFormatter& formatter, + const DatabaseConstraints& lookup, + ResourceType queryLevel, + const std::set<std::string>& labels, // New in Orthanc 1.12.0 + LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 + size_t limit); + +#if ORTHANC_PLUGINS_HAS_INTEGRATED_FIND == 1 + static void Apply(std::string& sql, + ISqlLookupFormatter& formatter, + const Orthanc::DatabasePluginMessages::Find_Request& request); +#endif + }; +}
--- a/Framework/Plugins/IndexUnitTests.h Mon Sep 09 12:48:52 2024 +0200 +++ b/Framework/Plugins/IndexUnitTests.h Mon Sep 09 15:04:48 2024 +0200 @@ -29,8 +29,6 @@ #include <Compatibility.h> // For std::unique_ptr<> -#include <orthanc/OrthancCDatabasePlugin.h> - #include <gtest/gtest.h> #include <list>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Plugins/MessagesToolbox.cpp Mon Sep 09 15:04:48 2024 +0200 @@ -0,0 +1,52 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "MessagesToolbox.h" + + +namespace OrthancDatabases +{ + namespace MessagesToolbox + { + Orthanc::ResourceType Convert(Orthanc::DatabasePluginMessages::ResourceType resourceType) + { + switch (resourceType) + { + case Orthanc::DatabasePluginMessages::RESOURCE_PATIENT: + return Orthanc::ResourceType_Patient; + + case Orthanc::DatabasePluginMessages::RESOURCE_STUDY: + return Orthanc::ResourceType_Study; + + case Orthanc::DatabasePluginMessages::RESOURCE_SERIES: + return Orthanc::ResourceType_Series; + + case Orthanc::DatabasePluginMessages::RESOURCE_INSTANCE: + return Orthanc::ResourceType_Instance; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Plugins/MessagesToolbox.h Mon Sep 09 15:04:48 2024 +0200 @@ -0,0 +1,67 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <orthanc/OrthancCDatabasePlugin.h> + +// Ensure that "ORTHANC_PLUGINS_VERSION_IS_ABOVE" is defined +#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" + +#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) // Macro introduced in Orthanc 1.3.1 +# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5) +# include <OrthancDatabasePlugin.pb.h> +# endif +#endif + + +#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0 + +#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) +# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 2) +# undef ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT +# define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 1 +# endif +#endif + + +#define ORTHANC_PLUGINS_HAS_INTEGRATED_FIND 0 + +#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) +# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 5) +# undef ORTHANC_PLUGINS_HAS_INTEGRATED_FIND +# define ORTHANC_PLUGINS_HAS_INTEGRATED_FIND 1 +# endif +#endif + + +#include <Enumerations.h> + + +namespace OrthancDatabases +{ + namespace MessagesToolbox + { + Orthanc::ResourceType Convert(Orthanc::DatabasePluginMessages::ResourceType resourceType); + } +}
--- a/Resources/CMake/DatabasesPluginConfiguration.cmake Mon Sep 09 12:48:52 2024 +0200 +++ b/Resources/CMake/DatabasesPluginConfiguration.cmake Mon Sep 09 15:04:48 2024 +0200 @@ -92,8 +92,6 @@ add_definitions( -DHAS_ORTHANC_EXCEPTION=1 - -DORTHANC_BUILDING_SERVER_LIBRARY=0 - -DORTHANC_ENABLE_PLUGINS=1 # To build "DatabaseConstraint.h" imported from Orthanc core -DORTHANC_OPTIMAL_VERSION_MAJOR=${ORTHANC_OPTIMAL_VERSION_MAJOR} -DORTHANC_OPTIMAL_VERSION_MINOR=${ORTHANC_OPTIMAL_VERSION_MINOR} -DORTHANC_OPTIMAL_VERSION_REVISION=${ORTHANC_OPTIMAL_VERSION_REVISION} @@ -114,10 +112,11 @@ ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV2.cpp ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV3.cpp ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseBackendAdapterV4.cpp + ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/DatabaseConstraint.cpp + ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/ISqlLookupFormatter.cpp ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexBackend.cpp ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexConnectionsPool.cpp + ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/MessagesToolbox.cpp ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/StorageBackend.cpp - ${ORTHANC_DATABASES_ROOT}/Resources/Orthanc/Databases/DatabaseConstraint.cpp - ${ORTHANC_DATABASES_ROOT}/Resources/Orthanc/Databases/ISqlLookupFormatter.cpp ${ORTHANC_DATABASES_ROOT}/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp )
--- a/Resources/Orthanc/Databases/DatabaseConstraint.cpp Mon Sep 09 12:48:52 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,348 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY) -# error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined -#endif - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../PrecompiledHeadersServer.h" -#endif - -#include "DatabaseConstraint.h" - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../../../OrthancFramework/Sources/OrthancException.h" -#else -# include <OrthancException.h> -#endif - -#include <boost/lexical_cast.hpp> -#include <cassert> - - -namespace Orthanc -{ - namespace Plugins - { -#if ORTHANC_ENABLE_PLUGINS == 1 - OrthancPluginResourceType Convert(ResourceType type) - { - switch (type) - { - case ResourceType_Patient: - return OrthancPluginResourceType_Patient; - - case ResourceType_Study: - return OrthancPluginResourceType_Study; - - case ResourceType_Series: - return OrthancPluginResourceType_Series; - - case ResourceType_Instance: - return OrthancPluginResourceType_Instance; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } -#endif - - -#if ORTHANC_ENABLE_PLUGINS == 1 - ResourceType Convert(OrthancPluginResourceType type) - { - switch (type) - { - case OrthancPluginResourceType_Patient: - return ResourceType_Patient; - - case OrthancPluginResourceType_Study: - return ResourceType_Study; - - case OrthancPluginResourceType_Series: - return ResourceType_Series; - - case OrthancPluginResourceType_Instance: - return ResourceType_Instance; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } -#endif - - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - OrthancPluginConstraintType Convert(ConstraintType constraint) - { - switch (constraint) - { - case ConstraintType_Equal: - return OrthancPluginConstraintType_Equal; - - case ConstraintType_GreaterOrEqual: - return OrthancPluginConstraintType_GreaterOrEqual; - - case ConstraintType_SmallerOrEqual: - return OrthancPluginConstraintType_SmallerOrEqual; - - case ConstraintType_Wildcard: - return OrthancPluginConstraintType_Wildcard; - - case ConstraintType_List: - return OrthancPluginConstraintType_List; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } -#endif - - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - ConstraintType Convert(OrthancPluginConstraintType constraint) - { - switch (constraint) - { - case OrthancPluginConstraintType_Equal: - return ConstraintType_Equal; - - case OrthancPluginConstraintType_GreaterOrEqual: - return ConstraintType_GreaterOrEqual; - - case OrthancPluginConstraintType_SmallerOrEqual: - return ConstraintType_SmallerOrEqual; - - case OrthancPluginConstraintType_Wildcard: - return ConstraintType_Wildcard; - - case OrthancPluginConstraintType_List: - return ConstraintType_List; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } -#endif - } - - DatabaseConstraint::DatabaseConstraint(ResourceType level, - const DicomTag& tag, - bool isIdentifier, - ConstraintType type, - const std::vector<std::string>& values, - bool caseSensitive, - bool mandatory) : - level_(level), - tag_(tag), - isIdentifier_(isIdentifier), - constraintType_(type), - values_(values), - caseSensitive_(caseSensitive), - mandatory_(mandatory) - { - if (type != ConstraintType_List && - values_.size() != 1) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - DatabaseConstraint::DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint) : - level_(Plugins::Convert(constraint.level)), - tag_(constraint.tagGroup, constraint.tagElement), - isIdentifier_(constraint.isIdentifierTag), - constraintType_(Plugins::Convert(constraint.type)), - caseSensitive_(constraint.isCaseSensitive), - mandatory_(constraint.isMandatory) - { - if (constraintType_ != ConstraintType_List && - constraint.valuesCount != 1) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - values_.resize(constraint.valuesCount); - - for (uint32_t i = 0; i < constraint.valuesCount; i++) - { - assert(constraint.values[i] != NULL); - values_[i].assign(constraint.values[i]); - } - } -#endif - - - const std::string& DatabaseConstraint::GetValue(size_t index) const - { - if (index >= values_.size()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - else - { - return values_[index]; - } - } - - - const std::string& DatabaseConstraint::GetSingleValue() const - { - if (values_.size() != 1) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - return values_[0]; - } - } - - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - void DatabaseConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint, - std::vector<const char*>& tmpValues) const - { - memset(&constraint, 0, sizeof(constraint)); - - tmpValues.resize(values_.size()); - - for (size_t i = 0; i < values_.size(); i++) - { - tmpValues[i] = values_[i].c_str(); - } - - constraint.level = Plugins::Convert(level_); - constraint.tagGroup = tag_.GetGroup(); - constraint.tagElement = tag_.GetElement(); - constraint.isIdentifierTag = isIdentifier_; - constraint.isCaseSensitive = caseSensitive_; - constraint.isMandatory = mandatory_; - constraint.type = Plugins::Convert(constraintType_); - constraint.valuesCount = values_.size(); - constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]); - } -#endif - - - void DatabaseConstraints::Clear() - { - for (size_t i = 0; i < constraints_.size(); i++) - { - assert(constraints_[i] != NULL); - delete constraints_[i]; - } - - constraints_.clear(); - } - - - void DatabaseConstraints::AddConstraint(DatabaseConstraint* constraint) - { - if (constraint == NULL) - { - throw OrthancException(ErrorCode_NullPointer); - } - else - { - constraints_.push_back(constraint); - } - } - - - const DatabaseConstraint& DatabaseConstraints::GetConstraint(size_t index) const - { - if (index >= constraints_.size()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - else - { - assert(constraints_[index] != NULL); - return *constraints_[index]; - } - } - - - std::string DatabaseConstraints::Format() const - { - std::string s; - - for (size_t i = 0; i < constraints_.size(); i++) - { - assert(constraints_[i] != NULL); - const DatabaseConstraint& constraint = *constraints_[i]; - s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) + - ": " + constraint.GetTag().Format(); - - switch (constraint.GetConstraintType()) - { - case ConstraintType_Equal: - s += " == " + constraint.GetSingleValue(); - break; - - case ConstraintType_SmallerOrEqual: - s += " <= " + constraint.GetSingleValue(); - break; - - case ConstraintType_GreaterOrEqual: - s += " >= " + constraint.GetSingleValue(); - break; - - case ConstraintType_Wildcard: - s += " ~~ " + constraint.GetSingleValue(); - break; - - case ConstraintType_List: - { - s += " in [ "; - bool first = true; - for (size_t j = 0; j < constraint.GetValuesCount(); j++) - { - if (first) - { - first = false; - } - else - { - s += ", "; - } - s += constraint.GetValue(j); - } - s += "]"; - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - s += "\n"; - } - - return s; - } -}
--- a/Resources/Orthanc/Databases/DatabaseConstraint.h Mon Sep 09 12:48:52 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,184 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY) -# error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined -#endif - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" -#else -// This is for the "orthanc-databases" project to reuse this file -# include <DicomFormat/DicomMap.h> -#endif - -#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0 - -#if ORTHANC_ENABLE_PLUGINS == 1 -# include <orthanc/OrthancCDatabasePlugin.h> -# if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) // Macro introduced in 1.3.1 -# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 2) -# undef ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT -# define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 1 -# endif -# endif -#endif - -#include <deque> - -namespace Orthanc -{ - enum ConstraintType - { - ConstraintType_Equal, - ConstraintType_SmallerOrEqual, - ConstraintType_GreaterOrEqual, - ConstraintType_Wildcard, - ConstraintType_List - }; - - namespace Plugins - { -#if ORTHANC_ENABLE_PLUGINS == 1 - OrthancPluginResourceType Convert(ResourceType type); -#endif - -#if ORTHANC_ENABLE_PLUGINS == 1 - ResourceType Convert(OrthancPluginResourceType type); -#endif - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - OrthancPluginConstraintType Convert(ConstraintType constraint); -#endif - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - ConstraintType Convert(OrthancPluginConstraintType constraint); -#endif - } - - - // This class is also used by the "orthanc-databases" project - class DatabaseConstraint : public boost::noncopyable - { - private: - ResourceType level_; - DicomTag tag_; - bool isIdentifier_; - ConstraintType constraintType_; - std::vector<std::string> values_; - bool caseSensitive_; - bool mandatory_; - - public: - DatabaseConstraint(ResourceType level, - const DicomTag& tag, - bool isIdentifier, - ConstraintType type, - const std::vector<std::string>& values, - bool caseSensitive, - bool mandatory); - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - explicit DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint); -#endif - - ResourceType GetLevel() const - { - return level_; - } - - const DicomTag& GetTag() const - { - return tag_; - } - - bool IsIdentifier() const - { - return isIdentifier_; - } - - ConstraintType GetConstraintType() const - { - return constraintType_; - } - - size_t GetValuesCount() const - { - return values_.size(); - } - - const std::string& GetValue(size_t index) const; - - const std::string& GetSingleValue() const; - - bool IsCaseSensitive() const - { - return caseSensitive_; - } - - bool IsMandatory() const - { - return mandatory_; - } - - bool IsMatch(const DicomMap& dicom) const; - -#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 - void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint, - std::vector<const char*>& tmpValues) const; -#endif - }; - - - class DatabaseConstraints : public boost::noncopyable - { - private: - std::deque<DatabaseConstraint*> constraints_; - - public: - ~DatabaseConstraints() - { - Clear(); - } - - void Clear(); - - void AddConstraint(DatabaseConstraint* constraint); // Takes ownership - - bool IsEmpty() const - { - return constraints_.empty(); - } - - size_t GetSize() const - { - return constraints_.size(); - } - - const DatabaseConstraint& GetConstraint(size_t index) const; - - std::string Format() const; - }; -}
--- a/Resources/Orthanc/Databases/ISqlLookupFormatter.cpp Mon Sep 09 12:48:52 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,876 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#if !defined(ORTHANC_BUILDING_SERVER_LIBRARY) -# error Macro ORTHANC_BUILDING_SERVER_LIBRARY must be defined -#endif - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../PrecompiledHeadersServer.h" -#endif - -#include "ISqlLookupFormatter.h" - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../../../OrthancFramework/Sources/OrthancException.h" -# include "../../../OrthancFramework/Sources/Toolbox.h" -# include "../Database/FindRequest.h" -#else -# include <OrthancException.h> -# include <Toolbox.h> -#endif - -#include "DatabaseConstraint.h" - -#include <cassert> -#include <boost/lexical_cast.hpp> -#include <list> - - -namespace Orthanc -{ - static std::string FormatLevel(ResourceType level) - { - switch (level) - { - case ResourceType_Patient: - return "patients"; - - case ResourceType_Study: - return "studies"; - - case ResourceType_Series: - return "series"; - - case ResourceType_Instance: - return "instances"; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - - static bool FormatComparison(std::string& target, - ISqlLookupFormatter& formatter, - const DatabaseConstraint& constraint, - size_t index, - bool escapeBrackets) - { - std::string tag = "t" + boost::lexical_cast<std::string>(index); - - std::string comparison; - - switch (constraint.GetConstraintType()) - { - case ConstraintType_Equal: - case ConstraintType_SmallerOrEqual: - case ConstraintType_GreaterOrEqual: - { - std::string op; - switch (constraint.GetConstraintType()) - { - case ConstraintType_Equal: - op = "="; - break; - - case ConstraintType_SmallerOrEqual: - op = "<="; - break; - - case ConstraintType_GreaterOrEqual: - op = ">="; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue()); - - if (constraint.IsCaseSensitive()) - { - comparison = tag + ".value " + op + " " + parameter; - } - else - { - comparison = "lower(" + tag + ".value) " + op + " lower(" + parameter + ")"; - } - - break; - } - - case ConstraintType_List: - { - for (size_t i = 0; i < constraint.GetValuesCount(); i++) - { - if (!comparison.empty()) - { - comparison += ", "; - } - - std::string parameter = formatter.GenerateParameter(constraint.GetValue(i)); - - if (constraint.IsCaseSensitive()) - { - comparison += parameter; - } - else - { - comparison += "lower(" + parameter + ")"; - } - } - - if (constraint.IsCaseSensitive()) - { - comparison = tag + ".value IN (" + comparison + ")"; - } - else - { - comparison = "lower(" + tag + ".value) IN (" + comparison + ")"; - } - - break; - } - - case ConstraintType_Wildcard: - { - const std::string value = constraint.GetSingleValue(); - - if (value == "*") - { - if (!constraint.IsMandatory()) - { - // Universal constraint on an optional tag, ignore it - return false; - } - } - else - { - std::string escaped; - escaped.reserve(value.size()); - - for (size_t i = 0; i < value.size(); i++) - { - if (value[i] == '*') - { - escaped += "%"; - } - else if (value[i] == '?') - { - escaped += "_"; - } - else if (value[i] == '%') - { - escaped += "\\%"; - } - else if (value[i] == '_') - { - escaped += "\\_"; - } - else if (value[i] == '\\') - { - escaped += "\\\\"; - } - else if (escapeBrackets && value[i] == '[') - { - escaped += "\\["; - } - else if (escapeBrackets && value[i] == ']') - { - escaped += "\\]"; - } - else - { - escaped += value[i]; - } - } - - std::string parameter = formatter.GenerateParameter(escaped); - - if (constraint.IsCaseSensitive()) - { - comparison = (tag + ".value LIKE " + parameter + " " + - formatter.FormatWildcardEscape()); - } - else - { - comparison = ("lower(" + tag + ".value) LIKE lower(" + - parameter + ") " + formatter.FormatWildcardEscape()); - } - } - - break; - } - - default: - return false; - } - - if (constraint.IsMandatory()) - { - target = comparison; - } - else if (comparison.empty()) - { - target = tag + ".value IS NULL"; - } - else - { - target = tag + ".value IS NULL OR " + comparison; - } - - return true; - } - - - static void FormatJoin(std::string& target, - const DatabaseConstraint& constraint, - size_t index) - { - std::string tag = "t" + boost::lexical_cast<std::string>(index); - - if (constraint.IsMandatory()) - { - target = " INNER JOIN "; - } - else - { - target = " LEFT JOIN "; - } - - if (constraint.IsIdentifier()) - { - target += "DicomIdentifiers "; - } - else - { - target += "MainDicomTags "; - } - - target += (tag + " ON " + tag + ".id = " + FormatLevel(constraint.GetLevel()) + - ".internalId AND " + tag + ".tagGroup = " + - boost::lexical_cast<std::string>(constraint.GetTag().GetGroup()) + - " AND " + tag + ".tagElement = " + - boost::lexical_cast<std::string>(constraint.GetTag().GetElement())); - } - - - static std::string Join(const std::list<std::string>& values, - const std::string& prefix, - const std::string& separator) - { - if (values.empty()) - { - return ""; - } - else - { - std::string s = prefix; - - bool first = true; - for (std::list<std::string>::const_iterator it = values.begin(); it != values.end(); ++it) - { - if (first) - { - first = false; - } - else - { - s += separator; - } - - s += *it; - } - - return s; - } - } - - static bool FormatComparison2(std::string& target, - ISqlLookupFormatter& formatter, - const DatabaseConstraint& constraint, - bool escapeBrackets) - { - std::string comparison; - std::string tagFilter = ("tagGroup = " + boost::lexical_cast<std::string>(constraint.GetTag().GetGroup()) - + " AND tagElement = " + boost::lexical_cast<std::string>(constraint.GetTag().GetElement())); - - switch (constraint.GetConstraintType()) - { - case ConstraintType_Equal: - case ConstraintType_SmallerOrEqual: - case ConstraintType_GreaterOrEqual: - { - std::string op; - switch (constraint.GetConstraintType()) - { - case ConstraintType_Equal: - op = "="; - break; - - case ConstraintType_SmallerOrEqual: - op = "<="; - break; - - case ConstraintType_GreaterOrEqual: - op = ">="; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue()); - - if (constraint.IsCaseSensitive()) - { - comparison = " AND value " + op + " " + parameter; - } - else - { - comparison = " AND lower(value) " + op + " lower(" + parameter + ")"; - } - - break; - } - - case ConstraintType_List: - { - std::vector<std::string> comparisonValues; - for (size_t i = 0; i < constraint.GetValuesCount(); i++) - { - std::string parameter = formatter.GenerateParameter(constraint.GetValue(i)); - - if (constraint.IsCaseSensitive()) - { - comparisonValues.push_back(parameter); - } - else - { - comparisonValues.push_back("lower(" + parameter + ")"); - } - } - - std::string values; - Toolbox::JoinStrings(values, comparisonValues, ", "); - - if (constraint.IsCaseSensitive()) - { - comparison = " AND value IN (" + values + ")"; - } - else - { - comparison = " AND lower(value) IN (" + values + ")"; - } - - break; - } - - case ConstraintType_Wildcard: - { - const std::string value = constraint.GetSingleValue(); - - if (value == "*") - { - if (!constraint.IsMandatory()) - { - // Universal constraint on an optional tag, ignore it - return false; - } - } - else - { - std::string escaped; - escaped.reserve(value.size()); - - for (size_t i = 0; i < value.size(); i++) - { - if (value[i] == '*') - { - escaped += "%"; - } - else if (value[i] == '?') - { - escaped += "_"; - } - else if (value[i] == '%') - { - escaped += "\\%"; - } - else if (value[i] == '_') - { - escaped += "\\_"; - } - else if (value[i] == '\\') - { - escaped += "\\\\"; - } - else if (escapeBrackets && value[i] == '[') - { - escaped += "\\["; - } - else if (escapeBrackets && value[i] == ']') - { - escaped += "\\]"; - } - else - { - escaped += value[i]; - } - } - - std::string parameter = formatter.GenerateParameter(escaped); - - if (constraint.IsCaseSensitive()) - { - comparison = " AND value LIKE " + parameter + " " + formatter.FormatWildcardEscape(); - } - else - { - comparison = " AND lower(value) LIKE lower(" + parameter + ") " + formatter.FormatWildcardEscape(); - } - } - - break; - } - - default: - return false; - } - - if (constraint.IsMandatory()) - { - target = tagFilter + comparison; - } - else if (comparison.empty()) - { - target = tagFilter + " AND value IS NULL"; - } - else - { - target = tagFilter + " AND value IS NULL OR " + comparison; - } - - return true; - } - - - void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel, - ResourceType& upperLevel, - const ResourceType& queryLevel, - const DatabaseConstraints& lookup) - { - assert(ResourceType_Patient < ResourceType_Study && - ResourceType_Study < ResourceType_Series && - ResourceType_Series < ResourceType_Instance); - - lowerLevel = queryLevel; - upperLevel = queryLevel; - - for (size_t i = 0; i < lookup.GetSize(); i++) - { - ResourceType level = lookup.GetConstraint(i).GetLevel(); - - if (level < upperLevel) - { - upperLevel = level; - } - - if (level > lowerLevel) - { - lowerLevel = level; - } - } - } - - - void ISqlLookupFormatter::Apply(std::string& sql, - ISqlLookupFormatter& formatter, - const DatabaseConstraints& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - size_t limit) - { - ResourceType lowerLevel, upperLevel; - GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup); - - assert(upperLevel <= queryLevel && - queryLevel <= lowerLevel); - - const bool escapeBrackets = formatter.IsEscapeBrackets(); - - std::string joins, comparisons; - - size_t count = 0; - - for (size_t i = 0; i < lookup.GetSize(); i++) - { - const DatabaseConstraint& constraint = lookup.GetConstraint(i); - - std::string comparison; - - if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets)) - { - std::string join; - FormatJoin(join, constraint, count); - joins += join; - - if (!comparison.empty()) - { - comparisons += " AND " + comparison; - } - - count ++; - } - } - - sql = ("SELECT " + - FormatLevel(queryLevel) + ".publicId, " + - FormatLevel(queryLevel) + ".internalId" + - " FROM Resources AS " + FormatLevel(queryLevel)); - - for (int level = queryLevel - 1; level >= upperLevel; level--) - { - sql += (" INNER JOIN Resources " + - FormatLevel(static_cast<ResourceType>(level)) + " ON " + - FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" + - FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId"); - } - - for (int level = queryLevel + 1; level <= lowerLevel; level++) - { - sql += (" INNER JOIN Resources " + - FormatLevel(static_cast<ResourceType>(level)) + " ON " + - FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" + - FormatLevel(static_cast<ResourceType>(level)) + ".parentId"); - } - - std::list<std::string> where; - where.push_back(FormatLevel(queryLevel) + ".resourceType = " + - formatter.FormatResourceType(queryLevel) + comparisons); - - if (!labels.empty()) - { - /** - * "In SQL Server, NOT EXISTS and NOT IN predicates are the best - * way to search for missing values, as long as both columns in - * question are NOT NULL." - * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/ - **/ - - std::list<std::string> formattedLabels; - for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) - { - formattedLabels.push_back(formatter.GenerateParameter(*it)); - } - - std::string condition; - switch (labelsConstraint) - { - case LabelsConstraint_Any: - condition = "> 0"; - break; - - case LabelsConstraint_All: - condition = "= " + boost::lexical_cast<std::string>(labels.size()); - break; - - case LabelsConstraint_None: - condition = "= 0"; - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + FormatLevel(queryLevel) + - ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition); - } - - sql += joins + Join(where, " WHERE ", " AND "); - - if (limit != 0) - { - sql += " LIMIT " + boost::lexical_cast<std::string>(limit); - } - } - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 - void ISqlLookupFormatter::Apply(std::string& sql, - ISqlLookupFormatter& formatter, - const FindRequest& request) - { - const bool escapeBrackets = formatter.IsEscapeBrackets(); - ResourceType queryLevel = request.GetLevel(); - const std::string& strQueryLevel = FormatLevel(queryLevel); - - ResourceType lowerLevel, upperLevel; - GetLookupLevels(lowerLevel, upperLevel, queryLevel, request.GetDicomTagConstraints()); - - assert(upperLevel <= queryLevel && - queryLevel <= lowerLevel); - - - sql = ("SELECT " + - strQueryLevel + ".publicId, " + - strQueryLevel + ".internalId" + - " FROM Resources AS " + strQueryLevel); - - - std::string joins, comparisons; - - if (request.GetOrthancIdentifiers().IsDefined() && request.GetOrthancIdentifiers().DetectLevel() <= queryLevel) - { - // single child resource matching, there should not be other constraints (at least for now) - assert(request.GetDicomTagConstraints().GetSize() == 0); - assert(request.GetLabels().size() == 0); - assert(request.HasLimits() == false); - - ResourceType topParentLevel = request.GetOrthancIdentifiers().DetectLevel(); - const std::string& strTopParentLevel = FormatLevel(topParentLevel); - - comparisons = " AND " + strTopParentLevel + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel)); - - for (int level = queryLevel; level > topParentLevel; level--) - { - sql += (" INNER JOIN Resources " + - FormatLevel(static_cast<ResourceType>(level - 1)) + " ON " + - FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" + - FormatLevel(static_cast<ResourceType>(level)) + ".parentId"); - } - } - else - { - size_t count = 0; - - const DatabaseConstraints& dicomTagsConstraints = request.GetDicomTagConstraints(); - for (size_t i = 0; i < dicomTagsConstraints.GetSize(); i++) - { - const DatabaseConstraint& constraint = dicomTagsConstraints.GetConstraint(i); - - std::string comparison; - - if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets)) - { - std::string join; - FormatJoin(join, constraint, count); - joins += join; - - if (!comparison.empty()) - { - comparisons += " AND " + comparison; - } - - count ++; - } - } - } - - for (int level = queryLevel - 1; level >= upperLevel; level--) - { - sql += (" INNER JOIN Resources " + - FormatLevel(static_cast<ResourceType>(level)) + " ON " + - FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" + - FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId"); - } - - for (int level = queryLevel + 1; level <= lowerLevel; level++) - { - sql += (" INNER JOIN Resources " + - FormatLevel(static_cast<ResourceType>(level)) + " ON " + - FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" + - FormatLevel(static_cast<ResourceType>(level)) + ".parentId"); - } - - std::list<std::string> where; - where.push_back(strQueryLevel + ".resourceType = " + - formatter.FormatResourceType(queryLevel) + comparisons); - - - if (!request.GetLabels().empty()) - { - /** - * "In SQL Server, NOT EXISTS and NOT IN predicates are the best - * way to search for missing values, as long as both columns in - * question are NOT NULL." - * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/ - **/ - - const std::set<std::string>& labels = request.GetLabels(); - std::list<std::string> formattedLabels; - for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) - { - formattedLabels.push_back(formatter.GenerateParameter(*it)); - } - - std::string condition; - switch (request.GetLabelsConstraint()) - { - case LabelsConstraint_Any: - condition = "> 0"; - break; - - case LabelsConstraint_All: - condition = "= " + boost::lexical_cast<std::string>(labels.size()); - break; - - case LabelsConstraint_None: - condition = "= 0"; - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - where.push_back("(SELECT COUNT(1) FROM Labels AS selectedLabels WHERE selectedLabels.id = " + strQueryLevel + - ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition); - } - - sql += joins + Join(where, " WHERE ", " AND "); - - if (request.HasLimits()) - { - sql += formatter.FormatLimits(request.GetLimitsSince(), request.GetLimitsCount()); - } - - } -#endif - - - void ISqlLookupFormatter::ApplySingleLevel(std::string& sql, - ISqlLookupFormatter& formatter, - const DatabaseConstraints& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, - LabelsConstraint labelsConstraint, - size_t limit - ) - { - ResourceType lowerLevel, upperLevel; - GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup); - - assert(upperLevel == queryLevel && - queryLevel == lowerLevel); - - const bool escapeBrackets = formatter.IsEscapeBrackets(); - - std::vector<std::string> mainDicomTagsComparisons, dicomIdentifiersComparisons; - - for (size_t i = 0; i < lookup.GetSize(); i++) - { - const DatabaseConstraint& constraint = lookup.GetConstraint(i); - - std::string comparison; - - if (FormatComparison2(comparison, formatter, constraint, escapeBrackets)) - { - if (!comparison.empty()) - { - if (constraint.IsIdentifier()) - { - dicomIdentifiersComparisons.push_back(comparison); - } - else - { - mainDicomTagsComparisons.push_back(comparison); - } - } - } - } - - sql = ("SELECT publicId, internalId " - "FROM Resources " - "WHERE resourceType = " + formatter.FormatResourceType(queryLevel) - + " "); - - if (dicomIdentifiersComparisons.size() > 0) - { - for (std::vector<std::string>::const_iterator it = dicomIdentifiersComparisons.begin(); it < dicomIdentifiersComparisons.end(); ++it) - { - sql += (" AND internalId IN (SELECT id FROM DicomIdentifiers WHERE " + *it + ") "); - } - } - - if (mainDicomTagsComparisons.size() > 0) - { - for (std::vector<std::string>::const_iterator it = mainDicomTagsComparisons.begin(); it < mainDicomTagsComparisons.end(); ++it) - { - sql += (" AND internalId IN (SELECT id FROM MainDicomTags WHERE " + *it + ") "); - } - } - - if (!labels.empty()) - { - /** - * "In SQL Server, NOT EXISTS and NOT IN predicates are the best - * way to search for missing values, as long as both columns in - * question are NOT NULL." - * https://explainextended.com/2009/09/15/not-in-vs-not-exists-vs-left-join-is-null-sql-server/ - **/ - - std::list<std::string> formattedLabels; - for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) - { - formattedLabels.push_back(formatter.GenerateParameter(*it)); - } - - std::string condition; - std::string inOrNotIn; - switch (labelsConstraint) - { - case LabelsConstraint_Any: - condition = "> 0"; - inOrNotIn = "IN"; - break; - - case LabelsConstraint_All: - condition = "= " + boost::lexical_cast<std::string>(labels.size()); - inOrNotIn = "IN"; - break; - - case LabelsConstraint_None: - condition = "> 0"; - inOrNotIn = "NOT IN"; - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - sql += (" AND internalId " + inOrNotIn + " (SELECT id" - " FROM (SELECT id, COUNT(1) AS labelsCount " - "FROM Labels " - "WHERE label IN (" + Join(formattedLabels, "", ", ") + ") GROUP BY id" - ") AS temp " - " WHERE labelsCount " + condition + ")"); - } - - if (limit != 0) - { - sql += " LIMIT " + boost::lexical_cast<std::string>(limit); - } - } - -}
--- a/Resources/Orthanc/Databases/ISqlLookupFormatter.h Mon Sep 09 12:48:52 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium - * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../../../OrthancFramework/Sources/Enumerations.h" -#else -# include <Enumerations.h> -#endif - -#include <boost/noncopyable.hpp> -#include <vector> - -namespace Orthanc -{ - class DatabaseConstraints; - class FindRequest; - - enum LabelsConstraint - { - LabelsConstraint_All, - LabelsConstraint_Any, - LabelsConstraint_None - }; - - // This class is also used by the "orthanc-databases" project - class ISqlLookupFormatter : public boost::noncopyable - { - public: - virtual ~ISqlLookupFormatter() - { - } - - virtual std::string GenerateParameter(const std::string& value) = 0; - - virtual std::string FormatResourceType(ResourceType level) = 0; - - virtual std::string FormatWildcardEscape() = 0; - - virtual std::string FormatLimits(uint64_t since, uint64_t count) = 0; - - /** - * Whether to escape '[' and ']', which is only needed for - * MSSQL. New in Orthanc 1.10.0, from the following changeset: - * https://orthanc.uclouvain.be/hg/orthanc-databases/rev/389c037387ea - **/ - virtual bool IsEscapeBrackets() const = 0; - - static void GetLookupLevels(ResourceType& lowerLevel, - ResourceType& upperLevel, - const ResourceType& queryLevel, - const DatabaseConstraints& lookup); - - static void Apply(std::string& sql, - ISqlLookupFormatter& formatter, - const DatabaseConstraints& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, // New in Orthanc 1.12.0 - LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 - size_t limit); - - static void ApplySingleLevel(std::string& sql, - ISqlLookupFormatter& formatter, - const DatabaseConstraints& lookup, - ResourceType queryLevel, - const std::set<std::string>& labels, // New in Orthanc 1.12.0 - LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 - size_t limit); - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 - static void Apply(std::string& sql, - ISqlLookupFormatter& formatter, - const FindRequest& request); -#endif - }; -}
--- a/Resources/SyncOrthancFolder.py Mon Sep 09 12:48:52 2024 +0200 +++ b/Resources/SyncOrthancFolder.py Mon Sep 09 15:04:48 2024 +0200 @@ -40,10 +40,6 @@ ('default', 'OrthancServer/Plugins/Samples/Common/OrthancPluginException.h', 'Plugins'), ('default', 'OrthancServer/Plugins/Samples/Common/OrthancPluginsExports.cmake', 'Plugins'), ('default', 'OrthancServer/Plugins/Samples/Common/VersionScriptPlugins.map', 'Plugins'), - ('find-refactoring', 'OrthancServer/Sources/Search/DatabaseConstraint.cpp', 'Databases'), - ('find-refactoring', 'OrthancServer/Sources/Search/DatabaseConstraint.h', 'Databases'), - ('find-refactoring', 'OrthancServer/Sources/Search/ISqlLookupFormatter.cpp', 'Databases'), - ('find-refactoring', 'OrthancServer/Sources/Search/ISqlLookupFormatter.h', 'Databases'), ] SDK = [