# HG changeset patch # User Sebastien Jodogne # Date 1725880715 -7200 # Node ID b8e6e7a194244c70e0ca11f8a9b83142f04b2ab7 # Parent a8f9d44e7842fc26d84b0ee6f14abf6dfce15a5b un-sharing DatabaseConstraint and ISqlLookupFormatter with Orthanc core diff -r a8f9d44e7842 -r b8e6e7a19424 Framework/Plugins/DatabaseConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Plugins/DatabaseConstraint.cpp Mon Sep 09 13:18:35 2024 +0200 @@ -0,0 +1,294 @@ +/** + * 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 . + **/ + + +/** + * 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/ + **/ + + +#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 +#endif + +#include + + +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& 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& 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]; + } + } +} diff -r a8f9d44e7842 -r b8e6e7a19424 Framework/Plugins/DatabaseConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Plugins/DatabaseConstraint.h Mon Sep 09 13:18:35 2024 +0200 @@ -0,0 +1,189 @@ +/** + * 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 . + **/ + + +/** + * 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 + +#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 +#endif + +#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0 + +#if ORTHANC_ENABLE_PLUGINS == 1 +# include +# 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 + +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 values_; + bool caseSensitive_; + bool mandatory_; + + public: + DatabaseConstraint(ResourceType level, + const DicomTag& tag, + bool isIdentifier, + ConstraintType type, + const std::vector& 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& tmpValues) const; +#endif + }; + + + class DatabaseConstraints : public boost::noncopyable + { + private: + std::deque 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; + }; +} diff -r a8f9d44e7842 -r b8e6e7a19424 Framework/Plugins/IDatabaseBackend.h --- a/Framework/Plugins/IDatabaseBackend.h Mon Sep 09 12:46:17 2024 +0200 +++ b/Framework/Plugins/IDatabaseBackend.h Mon Sep 09 13:18:35 2024 +0200 @@ -24,10 +24,10 @@ #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 diff -r a8f9d44e7842 -r b8e6e7a19424 Framework/Plugins/IDatabaseBackendOutput.h --- a/Framework/Plugins/IDatabaseBackendOutput.h Mon Sep 09 12:46:17 2024 +0200 +++ b/Framework/Plugins/IDatabaseBackendOutput.h Mon Sep 09 13:18:35 2024 +0200 @@ -23,7 +23,7 @@ #pragma once -#include "../../Resources/Orthanc/Databases/DatabaseConstraint.h" +#include "DatabaseConstraint.h" namespace OrthancDatabases { diff -r a8f9d44e7842 -r b8e6e7a19424 Framework/Plugins/ISqlLookupFormatter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Plugins/ISqlLookupFormatter.cpp Mon Sep 09 13:18:35 2024 +0200 @@ -0,0 +1,741 @@ +/** + * 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 . + **/ + + +/** + * 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/ + **/ + + +#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" +#else +# include +# include +#endif + +#include "DatabaseConstraint.h" + +#include +#include +#include + + +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(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(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(constraint.GetTag().GetGroup()) + + " AND " + tag + ".tagElement = " + + boost::lexical_cast(constraint.GetTag().GetElement())); + } + + + static std::string Join(const std::list& values, + const std::string& prefix, + const std::string& separator) + { + if (values.empty()) + { + return ""; + } + else + { + std::string s = prefix; + + bool first = true; + for (std::list::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(constraint.GetTag().GetGroup()) + + " AND tagElement = " + boost::lexical_cast(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 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& 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(level)) + " ON " + + FormatLevel(static_cast(level)) + ".internalId=" + + FormatLevel(static_cast(level + 1)) + ".parentId"); + } + + for (int level = queryLevel + 1; level <= lowerLevel; level++) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast(level)) + " ON " + + FormatLevel(static_cast(level - 1)) + ".internalId=" + + FormatLevel(static_cast(level)) + ".parentId"); + } + + std::list 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 formattedLabels; + for (std::set::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(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(limit); + } + } + + + void ISqlLookupFormatter::ApplySingleLevel(std::string& sql, + ISqlLookupFormatter& formatter, + const DatabaseConstraints& lookup, + ResourceType queryLevel, + const std::set& 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 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::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::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 formattedLabels; + for (std::set::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(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(limit); + } + } + +} diff -r a8f9d44e7842 -r b8e6e7a19424 Framework/Plugins/ISqlLookupFormatter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Plugins/ISqlLookupFormatter.h Mon Sep 09 13:18:35 2024 +0200 @@ -0,0 +1,95 @@ +/** + * 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 . + **/ + + +/** + * 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 + +#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 +# include "../../../OrthancFramework/Sources/Enumerations.h" +#else +# include +#endif + +#include +#include + +namespace Orthanc +{ + class DatabaseConstraints; + + 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; + + /** + * 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& 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& labels, // New in Orthanc 1.12.0 + LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 + size_t limit); + }; +} diff -r a8f9d44e7842 -r b8e6e7a19424 Resources/CMake/DatabasesPluginConfiguration.cmake --- a/Resources/CMake/DatabasesPluginConfiguration.cmake Mon Sep 09 12:46:17 2024 +0200 +++ b/Resources/CMake/DatabasesPluginConfiguration.cmake Mon Sep 09 13:18:35 2024 +0200 @@ -114,10 +114,10 @@ ${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/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 ) diff -r a8f9d44e7842 -r b8e6e7a19424 Resources/Orthanc/Databases/DatabaseConstraint.cpp --- a/Resources/Orthanc/Databases/DatabaseConstraint.cpp Mon Sep 09 12:46:17 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,287 +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 . - **/ - - -#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 -#endif - -#include - - -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& 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& 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]; - } - } -} diff -r a8f9d44e7842 -r b8e6e7a19424 Resources/Orthanc/Databases/DatabaseConstraint.h --- a/Resources/Orthanc/Databases/DatabaseConstraint.h Mon Sep 09 12:46:17 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,182 +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 . - **/ - - -#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 -#endif - -#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0 - -#if ORTHANC_ENABLE_PLUGINS == 1 -# include -# 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 - -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 values_; - bool caseSensitive_; - bool mandatory_; - - public: - DatabaseConstraint(ResourceType level, - const DicomTag& tag, - bool isIdentifier, - ConstraintType type, - const std::vector& 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& tmpValues) const; -#endif - }; - - - class DatabaseConstraints : public boost::noncopyable - { - private: - std::deque 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; - }; -} diff -r a8f9d44e7842 -r b8e6e7a19424 Resources/Orthanc/Databases/ISqlLookupFormatter.cpp --- a/Resources/Orthanc/Databases/ISqlLookupFormatter.cpp Mon Sep 09 12:46:17 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,734 +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 . - **/ - - -#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" -#else -# include -# include -#endif - -#include "DatabaseConstraint.h" - -#include -#include -#include - - -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(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(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(constraint.GetTag().GetGroup()) + - " AND " + tag + ".tagElement = " + - boost::lexical_cast(constraint.GetTag().GetElement())); - } - - - static std::string Join(const std::list& values, - const std::string& prefix, - const std::string& separator) - { - if (values.empty()) - { - return ""; - } - else - { - std::string s = prefix; - - bool first = true; - for (std::list::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(constraint.GetTag().GetGroup()) - + " AND tagElement = " + boost::lexical_cast(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 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& 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(level)) + " ON " + - FormatLevel(static_cast(level)) + ".internalId=" + - FormatLevel(static_cast(level + 1)) + ".parentId"); - } - - for (int level = queryLevel + 1; level <= lowerLevel; level++) - { - sql += (" INNER JOIN Resources " + - FormatLevel(static_cast(level)) + " ON " + - FormatLevel(static_cast(level - 1)) + ".internalId=" + - FormatLevel(static_cast(level)) + ".parentId"); - } - - std::list 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 formattedLabels; - for (std::set::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(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(limit); - } - } - - - void ISqlLookupFormatter::ApplySingleLevel(std::string& sql, - ISqlLookupFormatter& formatter, - const DatabaseConstraints& lookup, - ResourceType queryLevel, - const std::set& 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 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::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::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 formattedLabels; - for (std::set::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(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(limit); - } - } - -} diff -r a8f9d44e7842 -r b8e6e7a19424 Resources/Orthanc/Databases/ISqlLookupFormatter.h --- a/Resources/Orthanc/Databases/ISqlLookupFormatter.h Mon Sep 09 12:46:17 2024 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,88 +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 . - **/ - - -#pragma once - -#if ORTHANC_BUILDING_SERVER_LIBRARY == 1 -# include "../../../OrthancFramework/Sources/Enumerations.h" -#else -# include -#endif - -#include -#include - -namespace Orthanc -{ - class DatabaseConstraints; - - 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; - - /** - * 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& 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& labels, // New in Orthanc 1.12.0 - LabelsConstraint labelsConstraint, // New in Orthanc 1.12.0 - size_t limit); - }; -} diff -r a8f9d44e7842 -r b8e6e7a19424 Resources/SyncOrthancFolder.py --- a/Resources/SyncOrthancFolder.py Mon Sep 09 12:46:17 2024 +0200 +++ b/Resources/SyncOrthancFolder.py Mon Sep 09 13:18:35 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'), - ('default', 'OrthancServer/Sources/Search/DatabaseConstraint.cpp', 'Databases'), - ('default', 'OrthancServer/Sources/Search/DatabaseConstraint.h', 'Databases'), - ('default', 'OrthancServer/Sources/Search/ISqlLookupFormatter.cpp', 'Databases'), - ('default', 'OrthancServer/Sources/Search/ISqlLookupFormatter.h', 'Databases'), ] SDK = [