# HG changeset patch # User Sebastien Jodogne # Date 1539852802 -7200 # Node ID 9ff17eb830ec8eefa2c19ad30f699ff4c3709d2e # Parent f5ce33d3295c4735f65e151140535cd67e327bf6# Parent 3fabf9a673f65628842a03867470a6d6302e4577 integration db-changes->mainline diff -r f5ce33d3295c -r 9ff17eb830ec CMakeLists.txt --- a/CMakeLists.txt Mon Oct 15 15:33:40 2018 +0200 +++ b/CMakeLists.txt Thu Oct 18 10:53:22 2018 +0200 @@ -70,6 +70,8 @@ OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/QueryRetrieveHandler.cpp + OrthancServer/Search/DatabaseLookup.cpp + OrthancServer/Search/DicomTagConstraint.cpp OrthancServer/Search/HierarchicalMatcher.cpp OrthancServer/Search/IFindConstraint.cpp OrthancServer/Search/ListConstraint.cpp @@ -102,23 +104,24 @@ set(ORTHANC_UNIT_TESTS_SOURCES + UnitTestsSources/DatabaseLookupTests.cpp UnitTestsSources/DicomMapTests.cpp UnitTestsSources/FileStorageTests.cpp UnitTestsSources/FromDcmtkTests.cpp + UnitTestsSources/ImageProcessingTests.cpp + UnitTestsSources/ImageTests.cpp + UnitTestsSources/JpegLosslessTests.cpp + UnitTestsSources/LuaTests.cpp UnitTestsSources/MemoryCacheTests.cpp - UnitTestsSources/ImageTests.cpp + UnitTestsSources/MultiThreadingTests.cpp UnitTestsSources/RestApiTests.cpp + UnitTestsSources/SQLiteChromiumTests.cpp UnitTestsSources/SQLiteTests.cpp - UnitTestsSources/SQLiteChromiumTests.cpp UnitTestsSources/ServerIndexTests.cpp + UnitTestsSources/StreamTests.cpp + UnitTestsSources/UnitTestsMain.cpp UnitTestsSources/VersionsTests.cpp UnitTestsSources/ZipTests.cpp - UnitTestsSources/LuaTests.cpp - UnitTestsSources/MultiThreadingTests.cpp - UnitTestsSources/UnitTestsMain.cpp - UnitTestsSources/ImageProcessingTests.cpp - UnitTestsSources/JpegLosslessTests.cpp - UnitTestsSources/StreamTests.cpp ) diff -r f5ce33d3295c -r 9ff17eb830ec Core/HttpServer/MongooseServer.cpp --- a/Core/HttpServer/MongooseServer.cpp Mon Oct 15 15:33:40 2018 +0200 +++ b/Core/HttpServer/MongooseServer.cpp Thu Oct 18 10:53:22 2018 +0200 @@ -38,6 +38,7 @@ #include "../Logging.h" #include "../ChunkedBuffer.h" +#include "../OrthancException.h" #include "HttpToolbox.h" #if ORTHANC_ENABLE_MONGOOSE == 1 diff -r f5ce33d3295c -r 9ff17eb830ec Core/HttpServer/MongooseServer.h --- a/Core/HttpServer/MongooseServer.h Mon Oct 15 15:33:40 2018 +0200 +++ b/Core/HttpServer/MongooseServer.h Thu Oct 18 10:53:22 2018 +0200 @@ -49,8 +49,6 @@ #include "IIncomingHttpRequestFilter.h" -#include "../OrthancException.h" - #include #include #include @@ -60,6 +58,7 @@ namespace Orthanc { class ChunkStore; + class OrthancException; class IHttpExceptionFormatter : public boost::noncopyable { diff -r f5ce33d3295c -r 9ff17eb830ec Core/Images/ImageProcessing.cpp --- a/Core/Images/ImageProcessing.cpp Mon Oct 15 15:33:40 2018 +0200 +++ b/Core/Images/ImageProcessing.cpp Thu Oct 18 10:53:22 2018 +0200 @@ -35,6 +35,7 @@ #include "ImageProcessing.h" #include "PixelTraits.h" +#include "../OrthancException.h" #include diff -r f5ce33d3295c -r 9ff17eb830ec Core/Images/PixelTraits.h --- a/Core/Images/PixelTraits.h Mon Oct 15 15:33:40 2018 +0200 +++ b/Core/Images/PixelTraits.h Thu Oct 18 10:53:22 2018 +0200 @@ -34,7 +34,6 @@ #pragma once #include "../Enumerations.h" -#include "../OrthancException.h" #include @@ -55,10 +54,13 @@ ORTHANC_FORCE_INLINE static PixelType IntegerToPixel(int64_t value) { - if (value < static_cast(std::numeric_limits::min()) || - value > static_cast(std::numeric_limits::max())) + if (value < static_cast(std::numeric_limits::min())) { - throw OrthancException(ErrorCode_ParameterOutOfRange); + return std::numeric_limits::min(); + } + else if (value > static_cast(std::numeric_limits::max())) + { + return std::numeric_limits::max(); } else { diff -r f5ce33d3295c -r 9ff17eb830ec Core/MultiThreading/Semaphore.cpp diff -r f5ce33d3295c -r 9ff17eb830ec Core/MultiThreading/Semaphore.h diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/DicomInstanceToStore.cpp --- a/OrthancServer/DicomInstanceToStore.cpp Mon Oct 15 15:33:40 2018 +0200 +++ b/OrthancServer/DicomInstanceToStore.cpp Thu Oct 18 10:53:22 2018 +0200 @@ -35,7 +35,9 @@ #include "DicomInstanceToStore.h" #include "../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Core/DicomParsing/ParsedDicomFile.h" #include "../Core/Logging.h" +#include "../Core/OrthancException.h" #include #include @@ -43,161 +45,355 @@ namespace Orthanc { + // Anonymous namespace to avoid clashes between compilation modules + namespace + { + template + class SmartContainer + { + private: + T* content_; + bool toDelete_; + bool isReadOnly_; + + void Deallocate() + { + if (content_ && toDelete_) + { + delete content_; + toDelete_ = false; + content_ = NULL; + } + } + + public: + SmartContainer() : content_(NULL), toDelete_(false), isReadOnly_(true) + { + } + + ~SmartContainer() + { + Deallocate(); + } + + void Allocate() + { + Deallocate(); + content_ = new T; + toDelete_ = true; + isReadOnly_ = false; + } + + void TakeOwnership(T* content) + { + if (content == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + Deallocate(); + content_ = content; + toDelete_ = true; + isReadOnly_ = false; + } + + void SetReference(T& content) // Read and write assign, without transfering ownership + { + Deallocate(); + content_ = &content; + toDelete_ = false; + isReadOnly_ = false; + } + + void SetConstReference(const T& content) // Read-only assign, without transfering ownership + { + Deallocate(); + content_ = &const_cast(content); + toDelete_ = false; + isReadOnly_ = true; + } + + bool HasContent() const + { + return content_ != NULL; + } + + T& GetContent() + { + if (content_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (isReadOnly_) + { + throw OrthancException(ErrorCode_ReadOnly); + } + + return *content_; + } + + const T& GetConstContent() const + { + if (content_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return *content_; + } + }; + } + + + struct DicomInstanceToStore::PImpl + { + DicomInstanceOrigin origin_; + SmartContainer buffer_; + SmartContainer parsed_; + SmartContainer summary_; + SmartContainer json_; + MetadataMap metadata_; + + void ComputeMissingInformation() + { + if (buffer_.HasContent() && + summary_.HasContent() && + json_.HasContent()) + { + // Fine, everything is available + return; + } + + if (!buffer_.HasContent()) + { + if (!parsed_.HasContent()) + { + if (!summary_.HasContent()) + { + throw OrthancException(ErrorCode_NotImplemented); + } + else + { + parsed_.TakeOwnership(new ParsedDicomFile(summary_.GetConstContent())); + } + } + + // Serialize the parsed DICOM file + buffer_.Allocate(); + if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), + *parsed_.GetContent().GetDcmtkObject().getDataset())) + { + LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer"; + throw OrthancException(ErrorCode_InternalError); + } + } + + if (summary_.HasContent() && + json_.HasContent()) + { + return; + } + + // At this point, we know that the DICOM file is available as a + // memory buffer, but that its summary or its JSON version is + // missing + + if (!parsed_.HasContent()) + { + parsed_.TakeOwnership(new ParsedDicomFile(buffer_.GetConstContent())); + } + + // At this point, we have parsed the DICOM file + + if (!summary_.HasContent()) + { + summary_.Allocate(); + FromDcmtkBridge::ExtractDicomSummary(summary_.GetContent(), + *parsed_.GetContent().GetDcmtkObject().getDataset()); + } + + if (!json_.HasContent()) + { + json_.Allocate(); + + std::set ignoreTagLength; + FromDcmtkBridge::ExtractDicomAsJson(json_.GetContent(), + *parsed_.GetContent().GetDcmtkObject().getDataset(), + ignoreTagLength); + } + } + + + const char* GetBufferData() + { + ComputeMissingInformation(); + + if (!buffer_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (buffer_.GetConstContent().size() == 0) + { + return NULL; + } + else + { + return buffer_.GetConstContent().c_str(); + } + } + + + size_t GetBufferSize() + { + ComputeMissingInformation(); + + if (!buffer_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return buffer_.GetConstContent().size(); + } + + + const DicomMap& GetSummary() + { + ComputeMissingInformation(); + + if (!summary_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return summary_.GetConstContent(); + } + + + const Json::Value& GetJson() + { + ComputeMissingInformation(); + + if (!json_.HasContent()) + { + throw OrthancException(ErrorCode_InternalError); + } + + return json_.GetConstContent(); + } + + + bool LookupTransferSyntax(std::string& result) + { + ComputeMissingInformation(); + + DicomMap header; + if (DicomMap::ParseDicomMetaInformation(header, GetBufferData(), GetBufferSize())) + { + const DicomValue* value = header.TestAndGetValue(DICOM_TAG_TRANSFER_SYNTAX_UID); + if (value != NULL && + !value->IsBinary() && + !value->IsNull()) + { + result = Toolbox::StripSpaces(value->GetContent()); + return true; + } + } + + return false; + } + }; + + + DicomInstanceToStore::DicomInstanceToStore() : + pimpl_(new PImpl) + { + } + + + void DicomInstanceToStore::SetOrigin(const DicomInstanceOrigin& origin) + { + pimpl_->origin_ = origin; + } + + + const DicomInstanceOrigin& DicomInstanceToStore::GetOrigin() const + { + return pimpl_->origin_; + } + + + void DicomInstanceToStore::SetBuffer(const std::string& dicom) + { + pimpl_->buffer_.SetConstReference(dicom); + } + + + void DicomInstanceToStore::SetParsedDicomFile(ParsedDicomFile& parsed) + { + pimpl_->parsed_.SetReference(parsed); + } + + + void DicomInstanceToStore::SetSummary(const DicomMap& summary) + { + pimpl_->summary_.SetConstReference(summary); + } + + + void DicomInstanceToStore::SetJson(const Json::Value& json) + { + pimpl_->json_.SetConstReference(json); + } + + + const DicomInstanceToStore::MetadataMap& DicomInstanceToStore::GetMetadata() const + { + return pimpl_->metadata_; + } + + + DicomInstanceToStore::MetadataMap& DicomInstanceToStore::GetMetadata() + { + return pimpl_->metadata_; + } + + void DicomInstanceToStore::AddMetadata(ResourceType level, MetadataType metadata, const std::string& value) { - metadata_[std::make_pair(level, metadata)] = value; + pimpl_->metadata_[std::make_pair(level, metadata)] = value; } - void DicomInstanceToStore::ComputeMissingInformation() - { - if (buffer_.HasContent() && - summary_.HasContent() && - json_.HasContent()) - { - // Fine, everything is available - return; - } - - if (!buffer_.HasContent()) - { - if (!parsed_.HasContent()) - { - if (!summary_.HasContent()) - { - throw OrthancException(ErrorCode_NotImplemented); - } - else - { - parsed_.TakeOwnership(new ParsedDicomFile(summary_.GetConstContent())); - } - } - - // Serialize the parsed DICOM file - buffer_.Allocate(); - if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), - *parsed_.GetContent().GetDcmtkObject().getDataset())) - { - LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer"; - throw OrthancException(ErrorCode_InternalError); - } - } - - if (summary_.HasContent() && - json_.HasContent()) - { - return; - } - - // At this point, we know that the DICOM file is available as a - // memory buffer, but that its summary or its JSON version is - // missing - - if (!parsed_.HasContent()) - { - parsed_.TakeOwnership(new ParsedDicomFile(buffer_.GetConstContent())); - } - - // At this point, we have parsed the DICOM file - - if (!summary_.HasContent()) - { - summary_.Allocate(); - FromDcmtkBridge::ExtractDicomSummary(summary_.GetContent(), - *parsed_.GetContent().GetDcmtkObject().getDataset()); - } - - if (!json_.HasContent()) - { - json_.Allocate(); - - std::set ignoreTagLength; - FromDcmtkBridge::ExtractDicomAsJson(json_.GetContent(), - *parsed_.GetContent().GetDcmtkObject().getDataset(), - ignoreTagLength); - } - } - - - const char* DicomInstanceToStore::GetBufferData() { - ComputeMissingInformation(); - - if (!buffer_.HasContent()) - { - throw OrthancException(ErrorCode_InternalError); - } - - if (buffer_.GetConstContent().size() == 0) - { - return NULL; - } - else - { - return buffer_.GetConstContent().c_str(); - } + return pimpl_->GetBufferData(); } size_t DicomInstanceToStore::GetBufferSize() { - ComputeMissingInformation(); - - if (!buffer_.HasContent()) - { - throw OrthancException(ErrorCode_InternalError); - } - - return buffer_.GetConstContent().size(); + return pimpl_->GetBufferSize(); } const DicomMap& DicomInstanceToStore::GetSummary() { - ComputeMissingInformation(); - - if (!summary_.HasContent()) - { - throw OrthancException(ErrorCode_InternalError); - } - - return summary_.GetConstContent(); + return pimpl_->GetSummary(); } const Json::Value& DicomInstanceToStore::GetJson() { - ComputeMissingInformation(); - - if (!json_.HasContent()) - { - throw OrthancException(ErrorCode_InternalError); - } - - return json_.GetConstContent(); + return pimpl_->GetJson(); } bool DicomInstanceToStore::LookupTransferSyntax(std::string& result) { - ComputeMissingInformation(); - - DicomMap header; - if (DicomMap::ParseDicomMetaInformation(header, GetBufferData(), GetBufferSize())) - { - const DicomValue* value = header.TestAndGetValue(DICOM_TAG_TRANSFER_SYNTAX_UID); - if (value != NULL && - !value->IsBinary() && - !value->IsNull()) - { - result = Toolbox::StripSpaces(value->GetContent()); - return true; - } - } - - return false; + return pimpl_->LookupTransferSyntax(result); } } diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/DicomInstanceToStore.h --- a/OrthancServer/DicomInstanceToStore.h Mon Oct 15 15:33:40 2018 +0200 +++ b/OrthancServer/DicomInstanceToStore.h Thu Oct 18 10:53:22 2018 +0200 @@ -33,166 +33,48 @@ #pragma once -#include "../Core/DicomParsing/ParsedDicomFile.h" -#include "../Core/OrthancException.h" +#include "../Core/DicomFormat/DicomMap.h" #include "DicomInstanceOrigin.h" -#include "ServerIndex.h" +#include "ServerEnumerations.h" + +#include namespace Orthanc { + class ParsedDicomFile; + class DicomInstanceToStore { - private: - template - class SmartContainer - { - private: - T* content_; - bool toDelete_; - bool isReadOnly_; - - void Deallocate() - { - if (content_ && toDelete_) - { - delete content_; - toDelete_ = false; - content_ = NULL; - } - } - - public: - SmartContainer() : content_(NULL), toDelete_(false), isReadOnly_(true) - { - } - - ~SmartContainer() - { - Deallocate(); - } - - void Allocate() - { - Deallocate(); - content_ = new T; - toDelete_ = true; - isReadOnly_ = false; - } - - void TakeOwnership(T* content) - { - if (content == NULL) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - Deallocate(); - content_ = content; - toDelete_ = true; - isReadOnly_ = false; - } + public: + typedef std::map, std::string> MetadataMap; - void SetReference(T& content) // Read and write assign, without transfering ownership - { - Deallocate(); - content_ = &content; - toDelete_ = false; - isReadOnly_ = false; - } - - void SetConstReference(const T& content) // Read-only assign, without transfering ownership - { - Deallocate(); - content_ = &const_cast(content); - toDelete_ = false; - isReadOnly_ = true; - } - - bool HasContent() const - { - return content_ != NULL; - } - - T& GetContent() - { - if (content_ == NULL) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - if (isReadOnly_) - { - throw OrthancException(ErrorCode_ReadOnly); - } - - return *content_; - } - - const T& GetConstContent() const - { - if (content_ == NULL) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - return *content_; - } - }; - - DicomInstanceOrigin origin_; - SmartContainer buffer_; - SmartContainer parsed_; - SmartContainer summary_; - SmartContainer json_; - ServerIndex::MetadataMap metadata_; - - void ComputeMissingInformation(); + private: + struct PImpl; + boost::shared_ptr pimpl_; public: - void SetOrigin(const DicomInstanceOrigin& origin) - { - origin_ = origin; - } + DicomInstanceToStore(); + + void SetOrigin(const DicomInstanceOrigin& origin); - const DicomInstanceOrigin& GetOrigin() const - { - return origin_; - } + const DicomInstanceOrigin& GetOrigin() const; - void SetBuffer(const std::string& dicom) - { - buffer_.SetConstReference(dicom); - } + void SetBuffer(const std::string& dicom); - void SetParsedDicomFile(ParsedDicomFile& parsed) - { - parsed_.SetReference(parsed); - } + void SetParsedDicomFile(ParsedDicomFile& parsed); + + void SetSummary(const DicomMap& summary); - void SetSummary(const DicomMap& summary) - { - summary_.SetConstReference(summary); - } + void SetJson(const Json::Value& json); - void SetJson(const Json::Value& json) - { - json_.SetConstReference(json); - } + const MetadataMap& GetMetadata() const; + + MetadataMap& GetMetadata(); void AddMetadata(ResourceType level, MetadataType metadata, const std::string& value); - const ServerIndex::MetadataMap& GetMetadata() const - { - return metadata_; - } - - ServerIndex::MetadataMap& GetMetadata() - { - return metadata_; - } - const char* GetBufferData(); size_t GetBufferSize(); diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/LuaScripting.h --- a/OrthancServer/LuaScripting.h Mon Oct 15 15:33:40 2018 +0200 +++ b/OrthancServer/LuaScripting.h Thu Oct 18 10:53:22 2018 +0200 @@ -34,6 +34,7 @@ #pragma once #include "DicomInstanceToStore.h" +#include "ServerIndexChange.h" #include "ServerJobs/LuaJobManager.h" #include "../Core/MultiThreading/SharedMessageQueue.h" diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/OrthancFindRequestHandler.cpp --- a/OrthancServer/OrthancFindRequestHandler.cpp Mon Oct 15 15:33:40 2018 +0200 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Thu Oct 18 10:53:22 2018 +0200 @@ -613,6 +613,8 @@ if (FilterQueryTag(value, level, tag, manufacturer)) { + // TODO - Move this to "ResourceLookup::AddDicomConstraint()" + ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); // DICOM specifies that searches must be case sensitive, except diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/Search/DatabaseLookup.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DatabaseLookup.cpp Thu Oct 18 10:53:22 2018 +0200 @@ -0,0 +1,262 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "DatabaseLookup.h" + +#include "../ServerToolbox.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" + +namespace Orthanc +{ + void DatabaseLookup::LoadTags(ResourceType level) + { + const DicomTag* tags = NULL; + size_t size; + + ServerToolbox::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (tags_.find(tags[i]) == tags_.end()) + { + tags_[tags[i]] = TagInfo(DicomTagType_Identifier, level); + } + else + { + // These patient-level tags are copied in the study level + assert(level == ResourceType_Study && + (tags[i] == DICOM_TAG_PATIENT_ID || + tags[i] == DICOM_TAG_PATIENT_NAME || + tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE)); + } + } + + DicomMap::LoadMainDicomTags(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (tags_.find(tags[i]) == tags_.end()) + { + tags_[tags[i]] = TagInfo(DicomTagType_Main, level); + } + } + } + + + DatabaseLookup::DatabaseLookup() + { + LoadTags(ResourceType_Patient); + LoadTags(ResourceType_Study); + LoadTags(ResourceType_Series); + LoadTags(ResourceType_Instance); + } + + + DatabaseLookup::~DatabaseLookup() + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + delete constraints_[i]; + } + } + + + const DicomTagConstraint& DatabaseLookup::GetConstraint(size_t index) const + { + if (index >= constraints_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(constraints_[index] != NULL); + return *constraints_[index]; + } + } + + + void DatabaseLookup::AddConstraint(DicomTagConstraint* constraint) + { + if (constraint == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + constraints_.push_back(constraint); + + std::map::const_iterator tag = tags_.find(constraint->GetTag()); + + if (tag == tags_.end()) + { + constraint->SetTagInfo(DicomTagType_Generic, ResourceType_Instance); + } + else + { + constraint->SetTagInfo(tag->second.GetType(), tag->second.GetLevel()); + } + } + } + + + bool DatabaseLookup::IsMatch(const DicomMap& value) + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + if (!constraints_[i]->IsMatch(value)) + { + return false; + } + } + + return true; + } + + + void DatabaseLookup::AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitivePN) + { + ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); + + if (vr == ValueRepresentation_Sequence) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + /** + * DICOM specifies that searches must always be case sensitive, + * except for tags with a PN value representation. For PN, Orthanc + * uses the configuration option "CaseSensitivePN" to decide + * whether matching is case-sensitive or case-insensitive. + * + * Reference: DICOM PS 3.4 + * - C.2.2.2.1 ("Single Value Matching") + * - C.2.2.2.4 ("Wild Card Matching") + * http://medical.nema.org/Dicom/2011/11_04pu.pdf + * + * "Except for Attributes with a PN Value Representation, only + * entities with values which match exactly the value specified in the + * request shall match. This matching is case-sensitive, i.e., + * sensitive to the exact encoding of the key attribute value in + * character sets where a letter may have multiple encodings (e.g., + * based on its case, its position in a word, or whether it is + * accented) + * + * For Attributes with a PN Value Representation (e.g., Patient Name + * (0010,0010)), an application may perform literal matching that is + * either case-sensitive, or that is insensitive to some or all + * aspects of case, position, accent, or other character encoding + * variants." + * + * (0008,0018) UI SOPInstanceUID => Case-sensitive + * (0008,0050) SH AccessionNumber => Case-sensitive + * (0010,0020) LO PatientID => Case-sensitive + * (0020,000D) UI StudyInstanceUID => Case-sensitive + * (0020,000E) UI SeriesInstanceUID => Case-sensitive + **/ + bool caseSensitive = true; + if (vr == ValueRepresentation_PersonName) + { + caseSensitive = caseSensitivePN; + } + + if ((vr == ValueRepresentation_Date || + vr == ValueRepresentation_DateTime || + vr == ValueRepresentation_Time) && + dicomQuery.find('-') != std::string::npos) + { + /** + * Range matching is only defined for TM, DA and DT value + * representations. This code fixes issues 35 and 37. + * + * Reference: "Range matching is not defined for types of + * Attributes other than dates and times", DICOM PS 3.4, + * C.2.2.2.5 ("Range Matching"). + **/ + size_t separator = dicomQuery.find('-'); + std::string lower = dicomQuery.substr(0, separator); + std::string upper = dicomQuery.substr(separator + 1); + + if (!lower.empty()) + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive)); + } + + if (!upper.empty()) + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive)); + } + } + else if (dicomQuery.find('\\') != std::string::npos) + { + DicomTag fixedTag(tag); + + if (tag == DICOM_TAG_MODALITIES_IN_STUDY) + { + // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained + // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html + fixedTag = DICOM_TAG_MODALITY; + } + + std::auto_ptr constraint + (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive)); + + std::vector items; + Toolbox::TokenizeString(items, dicomQuery, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + constraint->AddValue(items[i]); + } + + AddConstraint(constraint.release()); + } + else if (dicomQuery.find('*') != std::string::npos || + dicomQuery.find('?') != std::string::npos) + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive)); + } + else + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_Equal, dicomQuery, caseSensitive)); + } + } +} diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/Search/DatabaseLookup.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DatabaseLookup.h Thu Oct 18 10:53:22 2018 +0200 @@ -0,0 +1,104 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "DicomTagConstraint.h" + +namespace Orthanc +{ + class DatabaseLookup : public boost::noncopyable + { + private: + class TagInfo + { + private: + DicomTagType type_; + ResourceType level_; + + public: + TagInfo() : + type_(DicomTagType_Generic), + level_(ResourceType_Instance) + { + } + + TagInfo(DicomTagType type, + ResourceType level) : + type_(type), + level_(level) + { + } + + DicomTagType GetType() const + { + return type_; + } + + ResourceType GetLevel() const + { + return level_; + } + }; + + std::vector constraints_; + std::map tags_; + + void LoadTags(ResourceType level); + + public: + DatabaseLookup(); + + ~DatabaseLookup(); + + void Reserve(size_t n) + { + constraints_.reserve(n); + } + + size_t GetConstraintsCount() const + { + return constraints_.size(); + } + + const DicomTagConstraint& GetConstraint(size_t index) const; + + void AddConstraint(DicomTagConstraint* constraint); // Takes ownership + + bool IsMatch(const DicomMap& value); + + void AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitivePN); + }; +} diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/Search/DicomTagConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DicomTagConstraint.cpp Thu Oct 18 10:53:22 2018 +0200 @@ -0,0 +1,281 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "DicomTagConstraint.h" + +#include "../../Core/OrthancException.h" +#include "../../Core/Toolbox.h" + +#include + +namespace Orthanc +{ + class DicomTagConstraint::NormalizedString : public boost::noncopyable + { + private: + const std::string& source_; + bool caseSensitive_; + std::string upper_; + + public: + NormalizedString(const std::string& source, + bool caseSensitive) : + source_(source), + caseSensitive_(caseSensitive) + { + if (!caseSensitive_) + { + upper_ = Toolbox::ToUpperCaseWithAccents(source); + } + } + + const std::string& GetValue() const + { + if (caseSensitive_) + { + return source_; + } + else + { + return upper_; + } + } + }; + + + class DicomTagConstraint::RegularExpression : public boost::noncopyable + { + private: + boost::regex regex_; + + public: + RegularExpression(const std::string& source, + bool caseSensitive) + { + NormalizedString normalized(source, caseSensitive); + regex_ = boost::regex(Toolbox::WildcardToRegularExpression(normalized.GetValue())); + } + + const boost::regex& GetValue() const + { + return regex_; + } + }; + + + DicomTagConstraint::DicomTagConstraint(const DicomTag& tag, + ConstraintType type, + const std::string& value, + bool caseSensitive) : + hasTagInfo_(false), + tagType_(DicomTagType_Generic), // Dummy initialization + level_(ResourceType_Patient), // Dummy initialization + tag_(tag), + constraintType_(type), + caseSensitive_(caseSensitive) + { + if (type == ConstraintType_Equal || + type == ConstraintType_SmallerOrEqual || + type == ConstraintType_GreaterOrEqual || + type == ConstraintType_Wildcard) + { + values_.insert(value); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (type != ConstraintType_Wildcard && + (value.find('*') != std::string::npos || + value.find('?') != std::string::npos)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + DicomTagConstraint::DicomTagConstraint(const DicomTag& tag, + ConstraintType type, + bool caseSensitive) : + hasTagInfo_(false), + tagType_(DicomTagType_Generic), // Dummy initialization + level_(ResourceType_Patient), // Dummy initialization + tag_(tag), + constraintType_(type), + caseSensitive_(caseSensitive) + { + if (type != ConstraintType_List) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void DicomTagConstraint::SetTagInfo(DicomTagType tagType, + ResourceType level) + { + hasTagInfo_ = true; + tagType_ = tagType; + level_ = level; + } + + + DicomTagType DicomTagConstraint::GetTagType() const + { + if (!hasTagInfo_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return tagType_; + } + } + + + const ResourceType DicomTagConstraint::GetLevel() const + { + if (!hasTagInfo_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return level_; + } + } + + + void DicomTagConstraint::AddValue(const std::string& value) + { + if (constraintType_ != ConstraintType_List) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + values_.insert(value); + } + } + + + const std::string& DicomTagConstraint::GetValue() const + { + if (constraintType_ == ConstraintType_List) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else if (values_.size() != 1) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + return *values_.begin(); + } + } + + + bool DicomTagConstraint::IsMatch(const std::string& value) + { + NormalizedString source(value, caseSensitive_); + + switch (constraintType_) + { + case ConstraintType_Equal: + { + NormalizedString reference(GetValue(), caseSensitive_); + return source.GetValue() == reference.GetValue(); + } + + case ConstraintType_SmallerOrEqual: + { + NormalizedString reference(GetValue(), caseSensitive_); + return source.GetValue() <= reference.GetValue(); + } + + case ConstraintType_GreaterOrEqual: + { + NormalizedString reference(GetValue(), caseSensitive_); + return source.GetValue() >= reference.GetValue(); + } + + case ConstraintType_Wildcard: + { + if (regex_.get() == NULL) + { + regex_.reset(new RegularExpression(GetValue(), caseSensitive_)); + } + + return boost::regex_match(source.GetValue(), regex_->GetValue()); + } + + case ConstraintType_List: + { + for (std::set::const_iterator + it = values_.begin(); it != values_.end(); ++it) + { + NormalizedString reference(*it, caseSensitive_); + if (source.GetValue() == reference.GetValue()) + { + return true; + } + } + + return false; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + bool DicomTagConstraint::IsMatch(const DicomMap& value) + { + const DicomValue* tmp = value.TestAndGetValue(tag_); + + if (tmp == NULL || + tmp->IsNull() || + tmp->IsBinary()) + { + return false; + } + else + { + return IsMatch(tmp->GetContent()); + } + } +} diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/Search/DicomTagConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DicomTagConstraint.h Thu Oct 18 10:53:22 2018 +0200 @@ -0,0 +1,109 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../ServerEnumerations.h" +#include "../../Core/DicomFormat/DicomMap.h" + +#include + +namespace Orthanc +{ + class DicomTagConstraint : public boost::noncopyable + { + private: + class NormalizedString; + class RegularExpression; + + bool hasTagInfo_; + DicomTagType tagType_; + ResourceType level_; + DicomTag tag_; + ConstraintType constraintType_; + std::set values_; + bool caseSensitive_; + + boost::shared_ptr regex_; + + public: + DicomTagConstraint(const DicomTag& tag, + ConstraintType type, + const std::string& value, + bool caseSensitive); + + DicomTagConstraint(const DicomTag& tag, + ConstraintType type, + bool caseSensitive); + + bool HasTagInfo() const + { + return hasTagInfo_; + } + + void SetTagInfo(DicomTagType tagType, + ResourceType level); + + DicomTagType GetTagType() const; + + const ResourceType GetLevel() const; + + const DicomTag& GetTag() const + { + return tag_; + } + + ConstraintType GetConstraintType() const + { + return constraintType_; + } + + bool IsCaseSensitive() const + { + return caseSensitive_; + } + + void AddValue(const std::string& value); + + const std::string& GetValue() const; + + const std::set& GetValues() const + { + return values_; + } + + bool IsMatch(const std::string& value); + + bool IsMatch(const DicomMap& value); + }; +} diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Mon Oct 15 15:33:40 2018 +0200 +++ b/OrthancServer/ServerContext.cpp Thu Oct 18 10:53:22 2018 +0200 @@ -316,8 +316,10 @@ { StorageAccessor accessor(area_); - DicomInstanceHasher hasher(dicom.GetSummary()); - resultPublicId = hasher.HashInstance(); + { + DicomInstanceHasher hasher(dicom.GetSummary()); + resultPublicId = hasher.HashInstance(); + } Json::Value simplifiedTags; ServerToolbox::SimplifyTags(simplifiedTags, dicom.GetJson(), DicomToJsonFormat_Human); diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Mon Oct 15 15:33:40 2018 +0200 +++ b/OrthancServer/ServerEnumerations.h Thu Oct 18 10:53:22 2018 +0200 @@ -64,6 +64,22 @@ IdentifierConstraintType_Wildcard /* Case sensitive, "*" or "?" are the only allowed wildcards */ }; + enum DicomTagType + { + DicomTagType_Identifier, // Tag that whose value is stored and indexed in the DB + DicomTagType_Main, // Tag that is stored in the DB (but not indexed) + DicomTagType_Generic // Tag that is only stored in the JSON files + }; + + enum ConstraintType + { + ConstraintType_Equal, + ConstraintType_SmallerOrEqual, + ConstraintType_GreaterOrEqual, + ConstraintType_Wildcard, + ConstraintType_List + }; + /** * WARNING: Do not change the explicit values in the enumerations diff -r f5ce33d3295c -r 9ff17eb830ec OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Mon Oct 15 15:33:40 2018 +0200 +++ b/OrthancServer/ServerIndex.h Thu Oct 18 10:53:22 2018 +0200 @@ -54,7 +54,7 @@ { public: typedef std::list Attachments; - typedef std::map< std::pair, std::string> MetadataMap; + typedef std::map, std::string> MetadataMap; private: class Listener; diff -r f5ce33d3295c -r 9ff17eb830ec UnitTestsSources/DatabaseLookupTests.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/DatabaseLookupTests.cpp Thu Oct 18 10:53:22 2018 +0200 @@ -0,0 +1,297 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "PrecompiledHeadersUnitTests.h" +#include "gtest/gtest.h" + +#include "../OrthancServer/Search/DatabaseLookup.h" +#include "../Core/OrthancException.h" + +using namespace Orthanc; + + +TEST(DatabaseLookup, SingleConstraint) +{ + { + ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, + "HEL*LO", true), OrthancException); + ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, + "HEL?LO", true), OrthancException); + ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, + true), OrthancException); + + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELLO", true); + ASSERT_TRUE(tag.IsMatch("HELLO")); + ASSERT_FALSE(tag.IsMatch("hello")); + + ASSERT_TRUE(tag.IsCaseSensitive()); + ASSERT_EQ(ConstraintType_Equal, tag.GetConstraintType()); + + ASSERT_FALSE(tag.HasTagInfo()); + ASSERT_THROW(tag.GetTagType(), OrthancException); + ASSERT_THROW(tag.GetLevel(), OrthancException); + + tag.SetTagInfo(DicomTagType_Identifier, ResourceType_Series); + ASSERT_TRUE(tag.HasTagInfo()); + ASSERT_EQ(DicomTagType_Identifier, tag.GetTagType()); + ASSERT_EQ(ResourceType_Series, tag.GetLevel()); + + DicomMap m; + ASSERT_FALSE(tag.IsMatch(m)); + m.SetNullValue(DICOM_TAG_PATIENT_NAME); + ASSERT_FALSE(tag.IsMatch(m)); + m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", true /* binary */); + ASSERT_FALSE(tag.IsMatch(m)); + m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false /* string */); + ASSERT_TRUE(tag.IsMatch(m)); + } + + { + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELlo", false); + ASSERT_TRUE(tag.IsMatch("HELLO")); + ASSERT_TRUE(tag.IsMatch("hello")); + + ASSERT_EQ("HELlo", tag.GetValue()); + } + + { + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*L?O", true); + ASSERT_TRUE(tag.IsMatch("HELLO")); + ASSERT_TRUE(tag.IsMatch("HELLLLLO")); + ASSERT_TRUE(tag.IsMatch("HELxO")); + ASSERT_FALSE(tag.IsMatch("hello")); + } + + { + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*l?o", false); + ASSERT_TRUE(tag.IsMatch("HELLO")); + ASSERT_TRUE(tag.IsMatch("HELLLLLO")); + ASSERT_TRUE(tag.IsMatch("HELxO")); + ASSERT_TRUE(tag.IsMatch("hello")); + + ASSERT_FALSE(tag.IsCaseSensitive()); + ASSERT_EQ(ConstraintType_Wildcard, tag.GetConstraintType()); + } + + { + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_SmallerOrEqual, "123", true); + ASSERT_TRUE(tag.IsMatch("120")); + ASSERT_TRUE(tag.IsMatch("123")); + ASSERT_FALSE(tag.IsMatch("124")); + } + + { + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_GreaterOrEqual, "123", true); + ASSERT_FALSE(tag.IsMatch("122")); + ASSERT_TRUE(tag.IsMatch("123")); + ASSERT_TRUE(tag.IsMatch("124")); + } + + { + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, true); + ASSERT_FALSE(tag.IsMatch("CT")); + ASSERT_FALSE(tag.IsMatch("MR")); + + tag.AddValue("CT"); + ASSERT_TRUE(tag.IsMatch("CT")); + ASSERT_FALSE(tag.IsMatch("MR")); + + tag.AddValue("MR"); + ASSERT_TRUE(tag.IsMatch("CT")); + ASSERT_TRUE(tag.IsMatch("MR")); + ASSERT_FALSE(tag.IsMatch("ct")); + ASSERT_FALSE(tag.IsMatch("mr")); + + ASSERT_THROW(tag.GetValue(), OrthancException); + ASSERT_EQ(2u, tag.GetValues().size()); + } + + { + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, false); + + tag.AddValue("ct"); + tag.AddValue("mr"); + + ASSERT_TRUE(tag.IsMatch("CT")); + ASSERT_TRUE(tag.IsMatch("MR")); + ASSERT_TRUE(tag.IsMatch("ct")); + ASSERT_TRUE(tag.IsMatch("mr")); + } +} + + + +TEST(DatabaseLookup, FromDicom) +{ + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true); + ASSERT_EQ(1u, lookup.GetConstraintsCount()); + ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType()); + ASSERT_EQ("HELLO", lookup.GetConstraint(0).GetValue()); + ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive()); + + ASSERT_TRUE(lookup.GetConstraint(0).HasTagInfo()); + ASSERT_EQ(DicomTagType_Identifier, lookup.GetConstraint(0).GetTagType()); + ASSERT_EQ(ResourceType_Patient, lookup.GetConstraint(0).GetLevel()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", false); + ASSERT_EQ(1u, lookup.GetConstraintsCount()); + + // This is *not* a PN VR => "false" above is *not* used + ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", true); + ASSERT_EQ(1u, lookup.GetConstraintsCount()); + ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", false); + ASSERT_EQ(1u, lookup.GetConstraintsCount()); + + // This is a PN VR => "false" above is used + ASSERT_FALSE(lookup.GetConstraint(0).IsCaseSensitive()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_SERIES_DESCRIPTION, "2012-2016", false); + + ASSERT_TRUE(lookup.GetConstraint(0).HasTagInfo()); + ASSERT_EQ(DicomTagType_Main, lookup.GetConstraint(0).GetTagType()); + ASSERT_EQ(ResourceType_Series, lookup.GetConstraint(0).GetLevel()); + + // This is not a data VR + ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-2016", false); + + // This is a data VR => range is effective + ASSERT_EQ(2u, lookup.GetConstraintsCount()); + + ASSERT_TRUE(lookup.GetConstraint(0).GetConstraintType() != lookup.GetConstraint(1).GetConstraintType()); + + for (size_t i = 0; i < 2; i++) + { + ASSERT_TRUE(lookup.GetConstraint(i).GetConstraintType() == ConstraintType_SmallerOrEqual || + lookup.GetConstraint(i).GetConstraintType() == ConstraintType_GreaterOrEqual); + } + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false); + + ASSERT_EQ(1u, lookup.GetConstraintsCount()); + ASSERT_EQ(ConstraintType_GreaterOrEqual, lookup.GetConstraint(0).GetConstraintType()); + ASSERT_EQ("2012", lookup.GetConstraint(0).GetValue()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "-2016", false); + + ASSERT_EQ(1u, lookup.GetConstraintsCount()); + ASSERT_EQ(DICOM_TAG_PATIENT_BIRTH_DATE, lookup.GetConstraint(0).GetTag()); + ASSERT_EQ(ConstraintType_SmallerOrEqual, lookup.GetConstraint(0).GetConstraintType()); + ASSERT_EQ("2016", lookup.GetConstraint(0).GetValue()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_MODALITIES_IN_STUDY, "CT\\MR", false); + + ASSERT_EQ(1u, lookup.GetConstraintsCount()); + ASSERT_EQ(DICOM_TAG_MODALITY, lookup.GetConstraint(0).GetTag()); + ASSERT_EQ(ResourceType_Series, lookup.GetConstraint(0).GetLevel()); + ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType()); + + const std::set& values = lookup.GetConstraint(0).GetValues(); + ASSERT_EQ(2u, values.size()); + ASSERT_TRUE(values.find("CT") != values.end()); + ASSERT_TRUE(values.find("MR") != values.end()); + ASSERT_TRUE(values.find("nope") == values.end()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "CT\\MR", false); + + ASSERT_EQ(1u, lookup.GetConstraintsCount()); + ASSERT_EQ(DICOM_TAG_STUDY_DESCRIPTION, lookup.GetConstraint(0).GetTag()); + ASSERT_EQ(ResourceType_Study, lookup.GetConstraint(0).GetLevel()); + ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType()); + + const std::set& values = lookup.GetConstraint(0).GetValues(); + ASSERT_EQ(2u, values.size()); + ASSERT_TRUE(values.find("CT") != values.end()); + ASSERT_TRUE(values.find("MR") != values.end()); + ASSERT_TRUE(values.find("nope") == values.end()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false); + + ASSERT_EQ(1u, lookup.GetConstraintsCount()); + ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false); + + ASSERT_EQ(1u, lookup.GetConstraintsCount()); + ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType()); + } + + { + DatabaseLookup lookup; + lookup.AddDicomConstraint(DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID, "TEST", false); + + ASSERT_TRUE(lookup.GetConstraint(0).HasTagInfo()); + ASSERT_EQ(DicomTagType_Generic, lookup.GetConstraint(0).GetTagType()); + ASSERT_EQ(ResourceType_Instance, lookup.GetConstraint(0).GetLevel()); + } +}