# HG changeset patch # User Sebastien Jodogne # Date 1600255801 -7200 # Node ID b1d528687e255aa5df6cbba2f6bd7dfc0e7eae0c # Parent 37310bb1cd306ec026c8a247689463f5386bb569# Parent db38b2ad4c4a12549d54673b63f4e9ae0ced9094 merge diff -r db38b2ad4c4a -r b1d528687e25 NEWS --- a/NEWS Wed Sep 16 11:24:08 2020 +0200 +++ b/NEWS Wed Sep 16 13:30:01 2020 +0200 @@ -5,10 +5,10 @@ ------- * New configuration options to enable HTTP peers identification through certificates: - "SslVerifyPeers" & "SslTrustedClientCertificates" -* New configuration option "SyncStorageArea" to commit the files on disk "inside" the DB - transaction and avoid DB - File system discrepencies in case of hard shutdown - of the machine running Orthanc. This comes with a cost: DICOM file ingestion is slower. + "SslVerifyPeers" and "SslTrustedClientCertificates" +* New configuration option "SyncStorageArea" to immediately commit the files onto the disk + (through fsync()), so as to avoid discrepencies between DB and filesystem in case of hard + shutdown of the machine running Orthanc. This slows down adding new files into Orthanc. Maintenance ----------- @@ -19,7 +19,8 @@ * When checking DICOM allowed methods, if there are multiple modalities with the same AET, differentiate them from the calling IP * Enable the access to raw frames in Philips ELSCINT1 proprietary compression -* Support empty key passwords when using client certificates +* Support empty key passwords when using HTTP client certificates +* Fix handling of "ModalitiesInStudy" (0008,0061) in C-FIND and "/tools/find" Version 1.7.3 (2020-08-24) diff -r db38b2ad4c4a -r b1d528687e25 OrthancServer/Resources/Configuration.json --- a/OrthancServer/Resources/Configuration.json Wed Sep 16 11:24:08 2020 +0200 +++ b/OrthancServer/Resources/Configuration.json Wed Sep 16 13:30:01 2020 +0200 @@ -147,20 +147,21 @@ // Whether or not SSL is enabled "SslEnabled" : false, - // Path to the SSL certificate used by the HTTP server. - // Certifcate must be stored in the PEM format. - // meaningful only if SslEnabled is true. - // The file must contain both the certificate and the private key. + // Path to the SSL certificate used by the HTTP server. The file + // must be stored in the PEM format, and must contain both the + // certificate and the private key. This option is only meaningful + // if "SslEnabled" is true. "SslCertificate" : "certificate.pem", - // Whether or not peer client certificates shall be checked. - // meaningfull only if SslEnabled is true + // Whether or not peer client certificates shall be checked. This + // option is only meaningfull if "SslEnabled" is true. "SslVerifyPeers" : false, - // Path to the SSL certificate(s) that are trusted to verify - // peers identify. - // Certifcate(s) must be stored in the PEM format. - // meaningfull only if SslVerifyPeers is true + // Path to a file containing the concatenation of the client SSL + // certificate(s) that are trusted to verify the identify of remote + // HTTP clients. The individual certificate(s) must be stored in the + // PEM format. This option is only meaningfull if "SslVerifyPeers" + // is true. "SslTrustedClientCertificates" : "trustedClientCertificates.pem", // Whether or not the password protection is enabled (using HTTP diff -r db38b2ad4c4a -r b1d528687e25 OrthancServer/Sources/Search/DatabaseLookup.cpp --- a/OrthancServer/Sources/Search/DatabaseLookup.cpp Wed Sep 16 11:24:08 2020 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp Wed Sep 16 13:30:01 2020 +0200 @@ -66,7 +66,7 @@ } - void DatabaseLookup::AddConstraint(DicomTagConstraint* constraint) + void DatabaseLookup::AddConstraintInternal(DicomTagConstraint* constraint) { if (constraint == NULL) { @@ -163,28 +163,20 @@ if (!lower.empty()) { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive, mandatoryTag)); + AddConstraintInternal(new DicomTagConstraint + (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive, mandatoryTag)); } if (!upper.empty()) { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive, mandatoryTag)); + AddConstraintInternal(new DicomTagConstraint + (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive, mandatoryTag)); } } - else if (tag == DICOM_TAG_MODALITIES_IN_STUDY || - dicomQuery.find('\\') != std::string::npos) + 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::unique_ptr constraint (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive, mandatoryTag)); @@ -196,7 +188,7 @@ constraint->AddValue(items[i]); } - AddConstraint(constraint.release()); + AddConstraintInternal(constraint.release()); } else if ( /** @@ -219,13 +211,13 @@ (dicomQuery.find('*') != std::string::npos || dicomQuery.find('?') != std::string::npos)) { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive, mandatoryTag)); + AddConstraintInternal(new DicomTagConstraint + (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive, mandatoryTag)); } else { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag)); + AddConstraintInternal(new DicomTagConstraint + (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag)); } } @@ -313,4 +305,34 @@ return true; } + + + std::string DatabaseLookup::Format() const + { + std::string s; + + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + s += ("Contraint " + boost::lexical_cast(i) + ": " + + constraints_[i]->Format() + "\n"); + } + + return s; + } + + + bool DatabaseLookup::HasTag(const DicomTag& tag) const + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + if (constraints_[i]->GetTag() == tag) + { + return true; + } + } + + return false; + } } diff -r db38b2ad4c4a -r b1d528687e25 OrthancServer/Sources/Search/DatabaseLookup.h --- a/OrthancServer/Sources/Search/DatabaseLookup.h Wed Sep 16 11:24:08 2020 +0200 +++ b/OrthancServer/Sources/Search/DatabaseLookup.h Wed Sep 16 13:30:01 2020 +0200 @@ -50,7 +50,7 @@ bool caseSensitive, bool mandatoryTag); - void AddConstraint(DicomTagConstraint* constraint); // Takes ownership + void AddConstraintInternal(DicomTagConstraint* constraint); // Takes ownership public: DatabaseLookup() @@ -87,6 +87,15 @@ bool caseSensitive, bool mandatoryTag); + void AddConstraint(const DicomTagConstraint& constraint) + { + AddConstraintInternal(new DicomTagConstraint(constraint)); + } + bool HasOnlyMainDicomTags() const; + + std::string Format() const; + + bool HasTag(const DicomTag& tag) const; }; } diff -r db38b2ad4c4a -r b1d528687e25 OrthancServer/Sources/Search/DicomTagConstraint.cpp --- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp Wed Sep 16 11:24:08 2020 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp Wed Sep 16 13:30:01 2020 +0200 @@ -154,6 +154,16 @@ } + DicomTagConstraint::DicomTagConstraint(const DicomTagConstraint& other) : + tag_(other.tag_), + constraintType_(other.constraintType_), + values_(other.values_), + caseSensitive_(other.caseSensitive_), + mandatory_(other.mandatory_) + { + } + + DicomTagConstraint::DicomTagConstraint(const DatabaseConstraint& constraint) : tag_(constraint.GetTag()), constraintType_(constraint.GetConstraintType()), @@ -333,7 +343,7 @@ s += *it; } - return s + "]"; + return s + " ]"; } default: diff -r db38b2ad4c4a -r b1d528687e25 OrthancServer/Sources/Search/DicomTagConstraint.h --- a/OrthancServer/Sources/Search/DicomTagConstraint.h Wed Sep 16 11:24:08 2020 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.h Wed Sep 16 13:30:01 2020 +0200 @@ -70,6 +70,8 @@ bool caseSensitive, bool mandatory); + DicomTagConstraint(const DicomTagConstraint& other); + DicomTagConstraint(const DatabaseConstraint& constraint); const DicomTag& GetTag() const @@ -77,6 +79,11 @@ return tag_; } + void SetTag(const DicomTag& tag) + { + tag_ = tag; + } + ConstraintType GetConstraintType() const { return constraintType_; diff -r db38b2ad4c4a -r b1d528687e25 OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Wed Sep 16 11:24:08 2020 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Sep 16 13:30:01 2020 +0200 @@ -34,10 +34,10 @@ #include "PrecompiledHeadersServer.h" #include "ServerContext.h" -#include "../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h" #include "../../OrthancFramework/Sources/Cache/SharedArchive.h" #include "../../OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h" #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h" #include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h" #include "../../OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h" @@ -936,12 +936,12 @@ } - void ServerContext::Apply(ILookupVisitor& visitor, - const DatabaseLookup& lookup, - ResourceType queryLevel, - size_t since, - size_t limit) - { + void ServerContext::ApplyInternal(ILookupVisitor& visitor, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t since, + size_t limit) + { unsigned int databaseLimit = (queryLevel == ResourceType_Instance ? limitFindInstances_ : limitFindResults_); @@ -994,7 +994,6 @@ continue; } -#if 1 // New in Orthanc 1.6.0: Only keep the main DICOM tags at the // level of interest for the query switch (queryLevel) @@ -1016,17 +1015,6 @@ default: throw OrthancException(ErrorCode_InternalError); } - - // Special case of the "Modality" at the study level, in order - // to deal with C-FIND on "ModalitiesInStudy" (0008,0061). - // Check out integration test "test_rest_modalities_in_study". - if (queryLevel == ResourceType_Study) - { - dicom.CopyTagIfExists(tmp, DICOM_TAG_MODALITY); - } -#else - dicom.Assign(tmp); // This emulates Orthanc <= 1.5.8 -#endif hasOnlyMainDicomTags = true; } @@ -1096,6 +1084,225 @@ } + + namespace + { + class ModalitiesInStudyVisitor : public ServerContext::ILookupVisitor + { + private: + class Study : public boost::noncopyable + { + private: + std::string orthancId_; + std::string instanceId_; + DicomMap mainDicomTags_; + Json::Value dicomAsJson_; + std::set modalitiesInStudy_; + + public: + Study(const std::string& instanceId, + const DicomMap& seriesTags) : + instanceId_(instanceId), + dicomAsJson_(Json::nullValue) + { + { + DicomMap tmp; + tmp.Assign(seriesTags); + tmp.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "dummy", false); + DicomInstanceHasher hasher(tmp); + orthancId_ = hasher.HashStudy(); + } + + mainDicomTags_.MergeMainDicomTags(seriesTags, ResourceType_Study); + mainDicomTags_.MergeMainDicomTags(seriesTags, ResourceType_Patient); + AddModality(seriesTags); + } + + void AddModality(const DicomMap& seriesTags) + { + std::string modality; + if (seriesTags.LookupStringValue(modality, DICOM_TAG_MODALITY, false) && + !modality.empty()) + { + modalitiesInStudy_.insert(modality); + } + } + + void SetDicomAsJson(const Json::Value& dicomAsJson) + { + dicomAsJson_ = dicomAsJson; + } + + const std::string& GetOrthancId() const + { + return orthancId_; + } + + const std::string& GetInstanceId() const + { + return instanceId_; + } + + const DicomMap& GetMainDicomTags() const + { + return mainDicomTags_; + } + + const Json::Value* GetDicomAsJson() const + { + if (dicomAsJson_.type() == Json::nullValue) + { + return NULL; + } + else + { + return &dicomAsJson_; + } + } + }; + + typedef std::map Studies; + + bool isDicomAsJsonNeeded_; + bool complete_; + Studies studies_; + + public: + ModalitiesInStudyVisitor(bool isDicomAsJsonNeeded) : + isDicomAsJsonNeeded_(isDicomAsJsonNeeded) + { + } + + ~ModalitiesInStudyVisitor() + { + for (Studies::const_iterator it = studies_.begin(); it != studies_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + studies_.clear(); + } + + virtual bool IsDicomAsJsonNeeded() const + { + return isDicomAsJsonNeeded_; + } + + virtual void MarkAsComplete() + { + complete_ = true; + } + + virtual void Visit(const std::string& publicId, + const std::string& instanceId, + const DicomMap& seriesTags, + const Json::Value* dicomAsJson) + { + std::string studyInstanceUid; + if (seriesTags.LookupStringValue(studyInstanceUid, DICOM_TAG_STUDY_INSTANCE_UID, false)) + { + Studies::iterator found = studies_.find(studyInstanceUid); + if (found == studies_.end()) + { + // New study + std::unique_ptr study(new Study(instanceId, seriesTags)); + + if (dicomAsJson != NULL) + { + study->SetDicomAsJson(*dicomAsJson); + } + + studies_[studyInstanceUid] = study.release(); + } + else + { + // Already existing study + found->second->AddModality(seriesTags); + } + } + } + + void Forward(ILookupVisitor& callerVisitor, + size_t since, + size_t limit) const + { + size_t index = 0; + size_t countForwarded = 0; + + for (Studies::const_iterator it = studies_.begin(); it != studies_.end(); ++it, index++) + { + if (limit == 0 || + (index >= since && + index < limit)) + { + assert(it->second != NULL); + const Study& study = *it->second; + + countForwarded++; + callerVisitor.Visit(study.GetOrthancId(), study.GetInstanceId(), + study.GetMainDicomTags(), study.GetDicomAsJson()); + } + } + + if (countForwarded == studies_.size()) + { + callerVisitor.MarkAsComplete(); + } + } + }; + } + + + void ServerContext::Apply(ILookupVisitor& visitor, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t since, + size_t limit) + { + if (queryLevel == ResourceType_Study && + lookup.HasTag(DICOM_TAG_MODALITIES_IN_STUDY)) + { + // Convert the study-level query, into a series-level query, + // where "ModalitiesInStudy" is replaced by "Modality" + DatabaseLookup seriesLookup; + + for (size_t i = 0; i < lookup.GetConstraintsCount(); i++) + { + const DicomTagConstraint& constraint = lookup.GetConstraint(i); + if (constraint.GetTag() == DICOM_TAG_MODALITIES_IN_STUDY) + { + if ((constraint.GetConstraintType() == ConstraintType_Equal && constraint.GetValue().empty()) || + (constraint.GetConstraintType() == ConstraintType_List && constraint.GetValues().empty())) + { + // Ignore universal lookup on "ModalitiesInStudy" (0008,0061), + // this should have been handled by the caller + return ApplyInternal(visitor, lookup, queryLevel, since, limit); + } + else + { + DicomTagConstraint modality(constraint); + modality.SetTag(DICOM_TAG_MODALITY); + seriesLookup.AddConstraint(modality); + } + } + else + { + seriesLookup.AddConstraint(constraint); + } + } + + ModalitiesInStudyVisitor seriesVisitor(visitor.IsDicomAsJsonNeeded()); + ApplyInternal(seriesVisitor, seriesLookup, ResourceType_Series, 0, 0); + seriesVisitor.Forward(visitor, since, limit); + } + else + { + ApplyInternal(visitor, lookup, queryLevel, since, limit); + } + } + + bool ServerContext::LookupOrReconstructMetadata(std::string& target, const std::string& publicId, MetadataType metadata) diff -r db38b2ad4c4a -r b1d528687e25 OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Wed Sep 16 11:24:08 2020 +0200 +++ b/OrthancServer/Sources/ServerContext.h Wed Sep 16 13:30:01 2020 +0200 @@ -80,6 +80,8 @@ virtual void MarkAsComplete() = 0; + // NB: "dicomAsJson" must *not* be deleted, and can be NULL if + // "!IsDicomAsJsonNeeded()" virtual void Visit(const std::string& publicId, const std::string& instanceId, const DicomMap& mainDicomTags, @@ -237,6 +239,12 @@ DicomInstanceToStore& dicom, StoreInstanceMode mode); + void ApplyInternal(ILookupVisitor& visitor, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t since, + size_t limit); + public: class DicomCacheLocker : public boost::noncopyable { diff -r db38b2ad4c4a -r b1d528687e25 OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp --- a/OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp Wed Sep 16 11:24:08 2020 +0200 +++ b/OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp Wed Sep 16 13:30:01 2020 +0200 @@ -154,6 +154,9 @@ ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType()); ASSERT_EQ("HELLO", lookup.GetConstraint(0).GetValue()); ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive()); + + ASSERT_TRUE(lookup.HasTag(DICOM_TAG_PATIENT_ID)); + ASSERT_FALSE(lookup.HasTag(DICOM_TAG_PATIENT_NAME)); } {