# HG changeset patch # User Sebastien Jodogne # Date 1545133827 -3600 # Node ID fd587cf51a89eb5c61f4139fe8ee93f08b1293e2 # Parent 039a9d262d64ad64219fdaca197a0d937bdc7906 cont diff -r 039a9d262d64 -r fd587cf51a89 Core/Enumerations.cpp --- a/Core/Enumerations.cpp Mon Dec 17 17:05:28 2018 +0100 +++ b/Core/Enumerations.cpp Tue Dec 18 12:50:27 2018 +0100 @@ -1897,6 +1897,35 @@ } + bool IsResourceLevelAboveOrEqual(ResourceType level, + ResourceType reference) + { + switch (reference) + { + case ResourceType_Patient: + return (level == ResourceType_Patient); + + case ResourceType_Study: + return (level == ResourceType_Patient || + level == ResourceType_Study); + + case ResourceType_Series: + return (level == ResourceType_Patient || + level == ResourceType_Study || + level == ResourceType_Series); + + case ResourceType_Instance: + return (level == ResourceType_Patient || + level == ResourceType_Study || + level == ResourceType_Series || + level == ResourceType_Instance); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + DicomModule GetModule(ResourceType type) { switch (type) diff -r 039a9d262d64 -r fd587cf51a89 Core/Enumerations.h --- a/Core/Enumerations.h Mon Dec 17 17:05:28 2018 +0100 +++ b/Core/Enumerations.h Tue Dec 18 12:50:27 2018 +0100 @@ -758,6 +758,9 @@ ResourceType GetParentResourceType(ResourceType type); + bool IsResourceLevelAboveOrEqual(ResourceType level, + ResourceType reference); + DicomModule GetModule(ResourceType type); const char* GetDicomSpecificCharacterSet(Encoding encoding); diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/IDatabaseWrapper.h --- a/OrthancServer/IDatabaseWrapper.h Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/IDatabaseWrapper.h Tue Dec 18 12:50:27 2018 +0100 @@ -226,15 +226,13 @@ virtual bool IsDiskSizeAbove(uint64_t threshold) = 0; - virtual void FindOneChildInstance(std::vector& instancesId, - const std::vector& resourcesId, - ResourceType level) = 0; - virtual void ApplyLookupPatients(std::vector& patientsId, + std::vector& instancesId, const DatabaseLookup& lookup, size_t limit) = 0; virtual void ApplyLookupResources(std::vector& resourcesId, + std::vector& instancesId, const DatabaseLookup& lookup, ResourceType queryLevel, size_t limit) = 0; diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Tue Dec 18 12:50:27 2018 +0100 @@ -1418,9 +1418,9 @@ query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), request[KEY_QUERY][members[i]].asString(), caseSensitive); - query2.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), - request[KEY_QUERY][members[i]].asString(), - caseSensitive, true); + query2.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), + request[KEY_QUERY][members[i]].asString(), + caseSensitive, true); } FindVisitor visitor; diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/SQLiteDatabaseWrapper.cpp --- a/OrthancServer/SQLiteDatabaseWrapper.cpp Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/SQLiteDatabaseWrapper.cpp Tue Dec 18 12:50:27 2018 +0100 @@ -339,6 +339,9 @@ db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;"); //db_.Execute("PRAGMA TEMP_STORE=memory"); + // Make "LIKE" case-sensitive in SQLite + db_.Execute("PRAGMA case_sensitive_like = true;"); + if (!db_.DoesTableExist("GlobalProperties")) { LOG(INFO) << "Creating the database"; @@ -1203,129 +1206,19 @@ } - namespace - { - class MainDicomTagsRegistry : public boost::noncopyable - { - private: - class TagInfo - { - private: - ResourceType level_; - DicomTagType type_; - - public: - TagInfo() - { - } - - TagInfo(ResourceType level, - DicomTagType type) : - level_(level), - type_(type) - { - } - - ResourceType GetLevel() const - { - return level_; - } - - DicomTagType GetType() const - { - return type_; - } - }; - - typedef std::map Registry; - - - Registry registry_; - - void LoadTags(ResourceType level) - { - const DicomTag* tags = NULL; - size_t size; - - ServerToolbox::LoadIdentifiers(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - if (registry_.find(tags[i]) == registry_.end()) - { - registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); - } - 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 (registry_.find(tags[i]) == registry_.end()) - { - registry_[tags[i]] = TagInfo(level, DicomTagType_Main); - } - } - } - - public: - MainDicomTagsRegistry() - { - LoadTags(ResourceType_Patient); - LoadTags(ResourceType_Study); - LoadTags(ResourceType_Series); - LoadTags(ResourceType_Instance); - } - - void LookupTag(ResourceType& level, - DicomTagType& type, - const DicomTag& tag) const - { - Registry::const_iterator it = registry_.find(tag); - - if (it == registry_.end()) - { - // Default values - level = ResourceType_Instance; - type = DicomTagType_Generic; - } - else - { - level = it->second.GetLevel(); - type = it->second.GetType(); - } - } - }; - } - - - void SQLiteDatabaseWrapper::FindOneChildInstance(std::vector& instancesId, - const std::vector& resourcesId, - ResourceType level) - { - printf("ICI 3\n"); - - throw OrthancException(ErrorCode_NotImplemented); - } - - void SQLiteDatabaseWrapper::ApplyLookupPatients(std::vector& patientsId, + std::vector& instancesId, const DatabaseLookup& lookup, size_t limit) { - static const MainDicomTagsRegistry registry; - printf("ICI 1\n"); - std::string heading = "SELECT patient.publicId FROM Resources AS patient "; + { + SQLite::Statement s(db_, "DROP TABLE IF EXISTS Lookup"); + s.Run(); + } + + std::string heading = "CREATE TEMPORARY TABLE Lookup AS SELECT patient.publicId, patient.internalId FROM Resources AS patient "; std::string trailer; std::vector parameters; @@ -1333,75 +1226,114 @@ { const DicomTagConstraint& constraint = lookup.GetConstraint(i); - ResourceType level; - DicomTagType type; - registry.LookupTag(level, type, constraint.GetTag()); - - if (level != ResourceType_Patient) + if (constraint.GetTagType() != DicomTagType_Identifier && + constraint.GetTagType() != DicomTagType_Main) { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Not a patient-level tag: (" + - lookup.GetConstraint(i).GetTag().Format() + ")"); + // This should have been set by ServerIndex::NormalizeLookup()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); } + + std::string tag = "t" + boost::lexical_cast(i); - if (type == DicomTagType_Identifier || - type == DicomTagType_Main) + char on[128]; + sprintf( + on, "%s JOIN %s %s ON %s.id = patient.internalId AND %s.tagGroup = 0x%04x AND %s.tagElement = 0x%04x ", + constraint.IsMandatory() ? "INNER" : "LEFT", + constraint.GetTagType() == DicomTagType_Identifier ? "DicomIdentifiers" : "MainDicomTags", + tag.c_str(), tag.c_str(), tag.c_str(), constraint.GetTag().GetGroup(), + tag.c_str(), constraint.GetTag().GetElement()); + + std::string comparison; + + switch (constraint.GetConstraintType()) { - std::string table = (type == DicomTagType_Identifier ? "DicomIdentifiers" : "MainDicomTags"); - std::string tag = "t" + boost::lexical_cast(i); + 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); + } + + parameters.push_back(constraint.GetValue()); - char on[128]; - sprintf(on, " %s ON %s.id = patient.internalId AND %s.tagGroup = 0x%04x AND %s.tagElement = 0x%04x ", - tag.c_str(), tag.c_str(), tag.c_str(), constraint.GetTag().GetGroup(), - tag.c_str(), constraint.GetTag().GetElement()); - - if (constraint.IsMandatory()) - { - heading += "INNER JOIN " + table + std::string(on); - } - else - { - heading += "LEFT JOIN " + table + std::string(on); + if (constraint.IsCaseSensitive()) + { + comparison = tag + ".value " + op + " ?"; + } + else + { + comparison = "lower(" + tag + ".value) " + op + " lower(?)"; + } + + break; } - trailer += "AND ("; - - if (!constraint.IsMandatory()) - { - trailer += tag + ".value IS NULL OR "; - } + case ConstraintType_List: + for (std::set::const_iterator + it = constraint.GetValues().begin(); + it != constraint.GetValues().end(); ++it) + { + parameters.push_back(*it); - if (constraint.IsCaseSensitive()) - { - trailer += tag + ".value "; - } - else - { - trailer += "lower(" + tag + ".value) "; - } - - switch (constraint.GetType()) - { - case ConstraintType_Equal: - parameters.push_back(constraint.GetValue()); + if (!comparison.empty()) + { + comparison += ", "; + } if (constraint.IsCaseSensitive()) { - trailer += "= ?"; + comparison += "?"; } else { - trailer += "= lower(?)"; + comparison += "lower(?)"; } + } - break; + if (constraint.IsCaseSensitive()) + { + comparison = tag + ".value IN (" + comparison + ")"; + } + else + { + comparison = "lower(" + tag + ".value) IN (" + comparison + ")"; + } + + break; - default: - throw OrthancException(ErrorCode_NotImplemented); - } + //case ConstraintType_Wildcard: + //break; + + default: + continue; + } + + heading += std::string(on); - trailer += ") "; + trailer += "AND ("; + + if (!constraint.IsMandatory()) + { + trailer += tag + ".value IS NULL OR "; } + + trailer += comparison + ") "; } if (limit != 0) @@ -1409,26 +1341,41 @@ trailer += " LIMIT " + boost::lexical_cast(limit); } - std::string sql = (heading + "WHERE patient.resourceType = " + - boost::lexical_cast(ResourceType_Patient) + " " + trailer); + { + std::string sql = (heading + "WHERE patient.resourceType = " + + boost::lexical_cast(ResourceType_Patient) + " " + trailer); - SQLite::Statement s(db_, sql); + printf("[%s]\n", sql.c_str()); + + SQLite::Statement s(db_, sql); - printf("[%s]\n", sql.c_str()); + for (size_t i = 0; i < parameters.size(); i++) + { + printf(" %lu = '%s'\n", i, parameters[i].c_str()); + s.BindString(i, parameters[i]); + } - for (size_t i = 0; i < parameters.size(); i++) - { - printf(" %d = '%s'\n", i, parameters[i].c_str()); - s.BindString(i, parameters[i]); + s.Run(); } - patientsId.clear(); + { + SQLite::Statement s + (db_, "SELECT patient.publicId, instances.publicID FROM Lookup AS patient " + "INNER JOIN Resources studies ON patient.internalId=studies.parentId " + "INNER JOIN Resources series ON studies.internalId=series.parentId " + "INNER JOIN Resources instances ON series.internalId=instances.parentId " + "GROUP BY patient.publicId"); + + patientsId.clear(); - while (s.Step()) - { - std::string publicId = s.ColumnString(0); - patientsId.push_back(publicId); - printf("** [%s]\n", publicId.c_str()); + while (s.Step()) + { + const std::string patient = s.ColumnString(0); + const std::string instance = s.ColumnString(1); + patientsId.push_back(patient); + instancesId.push_back(instance); + printf("** [%s] [%s]\n", patient.c_str(), instance.c_str()); + } } throw OrthancException(ErrorCode_NotImplemented); @@ -1436,6 +1383,7 @@ void SQLiteDatabaseWrapper::ApplyLookupResources(std::vector& resourcesId, + std::vector& instancesId, const DatabaseLookup& lookup, ResourceType queryLevel, size_t limit) diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/SQLiteDatabaseWrapper.h --- a/OrthancServer/SQLiteDatabaseWrapper.h Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/SQLiteDatabaseWrapper.h Tue Dec 18 12:50:27 2018 +0100 @@ -277,15 +277,13 @@ virtual bool IsDiskSizeAbove(uint64_t threshold); - virtual void FindOneChildInstance(std::vector& instancesId, - const std::vector& resourcesId, - ResourceType level); - virtual void ApplyLookupPatients(std::vector& patientsId, + std::vector& instancesId, const DatabaseLookup& lookup, size_t limit); virtual void ApplyLookupResources(std::vector& resourcesId, + std::vector& instancesId, const DatabaseLookup& lookup, ResourceType queryLevel, size_t limit); diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/Search/DatabaseLookup.cpp --- a/OrthancServer/Search/DatabaseLookup.cpp Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/Search/DatabaseLookup.cpp Tue Dec 18 12:50:27 2018 +0100 @@ -91,55 +91,12 @@ } - void DatabaseLookup::AddDicomConstraint(const DicomTag& tag, - const std::string& dicomQuery, - bool caseSensitivePN, - bool mandatoryTag) + void DatabaseLookup::AddDicomConstraintInternal(const DicomTag& tag, + ValueRepresentation vr, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag) { - 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) && @@ -205,4 +162,69 @@ (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag)); } } + + + void DatabaseLookup::AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitivePN, + bool mandatoryTag) + { + 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 + **/ + + if (vr == ValueRepresentation_PersonName) + { + AddDicomConstraintInternal(tag, vr, dicomQuery, caseSensitivePN, mandatoryTag); + } + else + { + AddDicomConstraintInternal(tag, vr, dicomQuery, true /* case sensitive */, mandatoryTag); + } + } + + + void DatabaseLookup::AddRestConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag) + { + AddDicomConstraintInternal(tag, FromDcmtkBridge::LookupValueRepresentation(tag), + dicomQuery, caseSensitive, mandatoryTag); + } } diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/Search/DatabaseLookup.h --- a/OrthancServer/Search/DatabaseLookup.h Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/Search/DatabaseLookup.h Tue Dec 18 12:50:27 2018 +0100 @@ -42,6 +42,11 @@ private: std::vector constraints_; + void AddDicomConstraintInternal(const DicomTag& tag, + ValueRepresentation vr, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag); public: DatabaseLookup() { @@ -69,5 +74,10 @@ const std::string& dicomQuery, bool caseSensitivePN, bool mandatoryTag); + + void AddRestConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag); }; } diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/Search/DicomTagConstraint.cpp --- a/OrthancServer/Search/DicomTagConstraint.cpp Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/Search/DicomTagConstraint.cpp Tue Dec 18 12:50:27 2018 +0100 @@ -100,6 +100,7 @@ bool caseSensitive, bool mandatory) : tag_(tag), + tagType_(DicomTagType_Generic), constraintType_(type), caseSensitive_(caseSensitive), mandatory_(mandatory) @@ -130,6 +131,7 @@ bool caseSensitive, bool mandatory) : tag_(tag), + tagType_(DicomTagType_Generic), constraintType_(type), caseSensitive_(caseSensitive), mandatory_(mandatory) @@ -141,6 +143,20 @@ } + DicomTagConstraint* DicomTagConstraint::Clone() const + { + std::auto_ptr clone(new DicomTagConstraint); + + clone->tag_ = tag_; + clone->constraintType_ = constraintType_; + clone->values_ = values_; + clone->caseSensitive_ = caseSensitive_; + clone->mandatory_ = mandatory_; + + return clone.release(); + } + + void DicomTagConstraint::AddValue(const std::string& value) { if (constraintType_ != ConstraintType_List) diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/Search/DicomTagConstraint.h --- a/OrthancServer/Search/DicomTagConstraint.h Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/Search/DicomTagConstraint.h Tue Dec 18 12:50:27 2018 +0100 @@ -47,6 +47,7 @@ class RegularExpression; DicomTag tag_; + DicomTagType tagType_; ConstraintType constraintType_; std::set values_; bool caseSensitive_; @@ -54,6 +55,12 @@ boost::shared_ptr regex_; + DicomTagConstraint() : + tag_(0, 0), + tagType_(DicomTagType_Generic) + { + } + public: DicomTagConstraint(const DicomTag& tag, ConstraintType type, @@ -67,12 +74,25 @@ bool caseSensitive, bool mandatory); + DicomTagConstraint* Clone() const; + const DicomTag& GetTag() const { return tag_; } - ConstraintType GetType() const + DicomTagType GetTagType() const + { + return tagType_; + } + + // Set by "ServerIndex::NormalizeLookup()" + void SetTagType(DicomTagType type) + { + tagType_ = type; + } + + ConstraintType GetConstraintType() const { return constraintType_; } @@ -82,6 +102,11 @@ return caseSensitive_; } + void SetCaseSensitive(bool caseSensitive) + { + caseSensitive_ = caseSensitive; + } + bool IsMandatory() const { return mandatory_; diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/ServerContext.cpp Tue Dec 18 12:50:27 2018 +0100 @@ -811,19 +811,31 @@ std::vector resources, instances; GetIndex().FindCandidates(resources, instances, lookup); + bool complete = true; + #if 1 { std::vector resources2, instances2; + size_t lookupLimit = (limit == 0 ? 0 : limit + 1); + if (lookup.GetLevel() == ResourceType_Patient) { - GetIndex().ApplyLookupPatients(resources2, instances2, lookup2, limit /* TODO */); + GetIndex().ApplyLookupPatients(resources2, instances2, lookup2, lookupLimit); } else { - GetIndex().ApplyLookupResources(resources2, instances2, lookup2, lookup.GetLevel(), limit /* TODO */); + GetIndex().ApplyLookupResources(resources2, instances2, lookup2, + lookup.GetLevel(), lookupLimit); } + if (limit != 0 && + resources2.size() > limit) + { + complete = false; + } + + // Sanity checks std::set r; for (size_t i = 0; i < resources2.size(); i++) { @@ -845,7 +857,6 @@ size_t countResults = 0; size_t skipped = 0; - bool complete = true; const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded(); diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/ServerIndex.cpp Tue Dec 18 12:50:27 2018 +0100 @@ -299,6 +299,107 @@ }; + class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable + { + private: + class TagInfo + { + private: + ResourceType level_; + DicomTagType type_; + + public: + TagInfo() + { + } + + TagInfo(ResourceType level, + DicomTagType type) : + level_(level), + type_(type) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + DicomTagType GetType() const + { + return type_; + } + }; + + typedef std::map Registry; + + + Registry registry_; + + void LoadTags(ResourceType level) + { + const DicomTag* tags = NULL; + size_t size; + + ServerToolbox::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (registry_.find(tags[i]) == registry_.end()) + { + registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); + } + 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 (registry_.find(tags[i]) == registry_.end()) + { + registry_[tags[i]] = TagInfo(level, DicomTagType_Main); + } + } + } + + public: + MainDicomTagsRegistry() + { + LoadTags(ResourceType_Patient); + LoadTags(ResourceType_Study); + LoadTags(ResourceType_Series); + LoadTags(ResourceType_Instance); + } + + void LookupTag(ResourceType& level, + DicomTagType& type, + const DicomTag& tag) const + { + Registry::const_iterator it = registry_.find(tag); + + if (it == registry_.end()) + { + // Default values + level = ResourceType_Instance; + type = DicomTagType_Generic; + } + else + { + level = it->second.GetLevel(); + type = it->second.GetType(); + } + } + }; + + bool ServerIndex::DeleteResource(Json::Value& target, const std::string& uuid, ResourceType expectedType) @@ -541,7 +642,8 @@ db_(db), maximumStorageSize_(0), maximumPatients_(0), - overwrite_(false) + overwrite_(false), + mainDicomTagsRegistry_(new MainDicomTagsRegistry) { listener_.reset(new Listener(context)); db_.SetListener(*listener_); @@ -2454,15 +2556,92 @@ } + void ServerIndex::NormalizeLookup(DatabaseLookup& target, + const DatabaseLookup& source, + ResourceType queryLevel) const + { + assert(mainDicomTagsRegistry_.get() != NULL); + + for (size_t i = 0; i < source.GetConstraintsCount(); i++) + { + const DicomTagConstraint& constraint = source.GetConstraint(i); + + ResourceType tagLevel; + DicomTagType tagType; + mainDicomTagsRegistry_->LookupTag(tagLevel, tagType, constraint.GetTag()); + + if (IsResourceLevelAboveOrEqual(tagLevel, queryLevel) && + (tagType == DicomTagType_Identifier || + tagType == DicomTagType_Main)) + { + if (tagType == DicomTagType_Identifier) + { + if (constraint.GetConstraintType() == ConstraintType_List) + { + if (!constraint.GetValues().empty()) + { + std::auto_ptr normalized( + new DicomTagConstraint(constraint.GetTag(), + ConstraintType_List, true, + constraint.IsMandatory())); + + normalized->SetTagType(tagType); + + for (std::set::const_iterator + value = constraint.GetValues().begin(); + value != constraint.GetValues().end(); ++value) + { + normalized->AddValue(ServerToolbox::NormalizeIdentifier(*value)); + } + + target.AddConstraint(normalized.release()); + } + } + else + { + std::string value = ServerToolbox::NormalizeIdentifier(constraint.GetValue()); + + std::auto_ptr normalized( + new DicomTagConstraint(constraint.GetTag(), + constraint.GetConstraintType(), + value, true, + constraint.IsMandatory())); + + normalized->SetTagType(tagType); + target.AddConstraint(normalized.release()); + } + } + else if (tagType == DicomTagType_Main) + { + std::auto_ptr clone(constraint.Clone()); + clone->SetTagType(tagType); + target.AddConstraint(clone.release()); + } + else if (tagType == DicomTagType_Generic) + { + // This tag is not indexed in the database, skip it + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + } + + void ServerIndex::ApplyLookupPatients(std::vector& patientsId, std::vector& instancesId, const DatabaseLookup& lookup, size_t limit) { - boost::mutex::scoped_lock lock(mutex_); - - db_.ApplyLookupPatients(patientsId, lookup, limit); - db_.FindOneChildInstance(instancesId, patientsId, ResourceType_Patient); + DatabaseLookup normalized; + NormalizeLookup(normalized, lookup, ResourceType_Patient); + + { + boost::mutex::scoped_lock lock(mutex_); + db_.ApplyLookupPatients(patientsId, instancesId, normalized, limit); + } } @@ -2472,9 +2651,12 @@ ResourceType queryLevel, size_t limit) { - boost::mutex::scoped_lock lock(mutex_); - - db_.ApplyLookupResources(resourcesId, lookup, queryLevel, limit); - db_.FindOneChildInstance(instancesId, resourcesId, queryLevel); + DatabaseLookup normalized; + NormalizeLookup(normalized, lookup, queryLevel); + + { + boost::mutex::scoped_lock lock(mutex_); + db_.ApplyLookupResources(resourcesId, instancesId, normalized, queryLevel, limit); + } } } diff -r 039a9d262d64 -r fd587cf51a89 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Mon Dec 17 17:05:28 2018 +0100 +++ b/OrthancServer/ServerIndex.h Tue Dec 18 12:50:27 2018 +0100 @@ -59,6 +59,7 @@ class Listener; class Transaction; class UnstableResourcePayload; + class MainDicomTagsRegistry; bool done_; boost::mutex mutex_; @@ -72,6 +73,7 @@ uint64_t maximumStorageSize_; unsigned int maximumPatients_; bool overwrite_; + std::auto_ptr mainDicomTagsRegistry_; static void FlushThread(ServerIndex* that, unsigned int threadSleep); @@ -125,6 +127,10 @@ MetadataType metadata, const std::string& value); + void NormalizeLookup(DatabaseLookup& target, + const DatabaseLookup& source, + ResourceType level) const; + public: ServerIndex(ServerContext& context, IDatabaseWrapper& database, diff -r 039a9d262d64 -r fd587cf51a89 Plugins/Engine/OrthancPluginDatabase.cpp --- a/Plugins/Engine/OrthancPluginDatabase.cpp Mon Dec 17 17:05:28 2018 +0100 +++ b/Plugins/Engine/OrthancPluginDatabase.cpp Tue Dec 18 12:50:27 2018 +0100 @@ -1162,15 +1162,8 @@ } - void OrthancPluginDatabase::FindOneChildInstance(std::vector& instancesId, - const std::vector& resourcesId, - ResourceType level) - { - throw OrthancException(ErrorCode_NotImplemented); - } - - void OrthancPluginDatabase::ApplyLookupPatients(std::vector& patientsId, + std::vector& instancesId, const DatabaseLookup& lookup, size_t limit) { @@ -1179,6 +1172,7 @@ void OrthancPluginDatabase::ApplyLookupResources(std::vector& patientsId, + std::vector& instancesId, const DatabaseLookup& lookup, ResourceType queryLevel, size_t limit) diff -r 039a9d262d64 -r fd587cf51a89 Plugins/Engine/OrthancPluginDatabase.h --- a/Plugins/Engine/OrthancPluginDatabase.h Mon Dec 17 17:05:28 2018 +0100 +++ b/Plugins/Engine/OrthancPluginDatabase.h Tue Dec 18 12:50:27 2018 +0100 @@ -271,15 +271,13 @@ virtual bool IsDiskSizeAbove(uint64_t threshold); - virtual void FindOneChildInstance(std::vector& instancesId, - const std::vector& resourcesId, - ResourceType level); - virtual void ApplyLookupPatients(std::vector& patientsId, + std::vector& instancesId, const DatabaseLookup& lookup, size_t limit); virtual void ApplyLookupResources(std::vector& patientsId, + std::vector& instancesId, const DatabaseLookup& lookup, ResourceType queryLevel, size_t limit); diff -r 039a9d262d64 -r fd587cf51a89 UnitTestsSources/DatabaseLookupTests.cpp --- a/UnitTestsSources/DatabaseLookupTests.cpp Mon Dec 17 17:05:28 2018 +0100 +++ b/UnitTestsSources/DatabaseLookupTests.cpp Tue Dec 18 12:50:27 2018 +0100 @@ -55,7 +55,7 @@ ASSERT_FALSE(tag.IsMatch("hello")); ASSERT_TRUE(tag.IsCaseSensitive()); - ASSERT_EQ(ConstraintType_Equal, tag.GetType()); + ASSERT_EQ(ConstraintType_Equal, tag.GetConstraintType()); DicomMap m; ASSERT_FALSE(tag.IsMatch(m)); @@ -91,7 +91,7 @@ ASSERT_TRUE(tag.IsMatch("hello")); ASSERT_FALSE(tag.IsCaseSensitive()); - ASSERT_EQ(ConstraintType_Wildcard, tag.GetType()); + ASSERT_EQ(ConstraintType_Wildcard, tag.GetConstraintType()); } { @@ -150,7 +150,7 @@ DatabaseLookup lookup; lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); - ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetType()); + ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType()); ASSERT_EQ("HELLO", lookup.GetConstraint(0).GetValue()); ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive()); } @@ -185,7 +185,7 @@ lookup.AddDicomConstraint(DICOM_TAG_SERIES_DESCRIPTION, "2012-2016", false, true); // This is not a data VR - ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetType()); + ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType()); } { @@ -195,12 +195,12 @@ // This is a data VR => range is effective ASSERT_EQ(2u, lookup.GetConstraintsCount()); - ASSERT_TRUE(lookup.GetConstraint(0).GetType() != lookup.GetConstraint(1).GetType()); + ASSERT_TRUE(lookup.GetConstraint(0).GetConstraintType() != lookup.GetConstraint(1).GetConstraintType()); for (size_t i = 0; i < 2; i++) { - ASSERT_TRUE(lookup.GetConstraint(i).GetType() == ConstraintType_SmallerOrEqual || - lookup.GetConstraint(i).GetType() == ConstraintType_GreaterOrEqual); + ASSERT_TRUE(lookup.GetConstraint(i).GetConstraintType() == ConstraintType_SmallerOrEqual || + lookup.GetConstraint(i).GetConstraintType() == ConstraintType_GreaterOrEqual); } } @@ -209,7 +209,7 @@ lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); - ASSERT_EQ(ConstraintType_GreaterOrEqual, lookup.GetConstraint(0).GetType()); + ASSERT_EQ(ConstraintType_GreaterOrEqual, lookup.GetConstraint(0).GetConstraintType()); ASSERT_EQ("2012", lookup.GetConstraint(0).GetValue()); } @@ -219,7 +219,7 @@ ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(DICOM_TAG_PATIENT_BIRTH_DATE, lookup.GetConstraint(0).GetTag()); - ASSERT_EQ(ConstraintType_SmallerOrEqual, lookup.GetConstraint(0).GetType()); + ASSERT_EQ(ConstraintType_SmallerOrEqual, lookup.GetConstraint(0).GetConstraintType()); ASSERT_EQ("2016", lookup.GetConstraint(0).GetValue()); } @@ -229,7 +229,7 @@ ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(DICOM_TAG_MODALITY, lookup.GetConstraint(0).GetTag()); - ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetType()); + ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType()); const std::set& values = lookup.GetConstraint(0).GetValues(); ASSERT_EQ(2u, values.size()); @@ -244,7 +244,7 @@ ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(DICOM_TAG_STUDY_DESCRIPTION, lookup.GetConstraint(0).GetTag()); - ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetType()); + ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType()); const std::set& values = lookup.GetConstraint(0).GetValues(); ASSERT_EQ(2u, values.size()); @@ -258,7 +258,7 @@ lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); - ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetType()); + ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType()); } { @@ -266,7 +266,7 @@ lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); - ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetType()); + ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType()); } { diff -r 039a9d262d64 -r fd587cf51a89 UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Mon Dec 17 17:05:28 2018 +0100 +++ b/UnitTestsSources/UnitTestsMain.cpp Tue Dec 18 12:50:27 2018 +0100 @@ -756,10 +756,29 @@ ASSERT_EQ(MimeType_Xml, StringToMimeType("text/xml")); ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml))); ASSERT_THROW(StringToMimeType("nope"), OrthancException); + + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Patient)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Study)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Instance)); + + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Patient)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Study)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Instance)); + + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Patient)); + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Study)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Instance)); + + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Patient)); + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Study)); + ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Series)); + ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Instance)); } - #if defined(__linux__) || defined(__OpenBSD__) #include #elif defined(__FreeBSD__)