# HG changeset patch # User Alain Mazy # Date 1548323719 -3600 # Node ID fc9a4a2dad63f71f4ce29d88fadb9a5871ee360d # Parent 4cfed5c2eacd850db5654f46dbb0f8192d07c781# Parent b6e7714c3fe6c207fc48365900e97674886bf628 merge diff -r 4cfed5c2eacd -r fc9a4a2dad63 .travis.yml --- a/.travis.yml Thu Jan 24 10:54:47 2019 +0100 +++ b/.travis.yml Thu Jan 24 10:55:19 2019 +0100 @@ -43,7 +43,7 @@ - cd Build - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == OFF ]; then cmake -DCMAKE_BUILD_TYPE=Debug "-DDCMTK_LIBRARIES=CharLS;dcmjpls;wrap;oflog" - -DALLOW_DOWNLOADS=ON -DUSE_SYSTEM_BOOST=OFF -DUSE_SYSTEM_MONGOOSE=OFF -DUSE_SYSTEM_JSONCPP=OFF + -DALLOW_DOWNLOADS=ON -DUSE_SYSTEM_BOOST=OFF -DUSE_SYSTEM_CIVETWEB=OFF -DUSE_SYSTEM_JSONCPP=OFF -DUSE_SYSTEM_GOOGLE_LOG=OFF -DUSE_SYSTEM_PUGIXML=OFF -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON ..; fi - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then cmake diff -r 4cfed5c2eacd -r fc9a4a2dad63 CMakeLists.txt --- a/CMakeLists.txt Thu Jan 24 10:54:47 2019 +0100 +++ b/CMakeLists.txt Thu Jan 24 10:55:19 2019 +0100 @@ -53,7 +53,13 @@ ##################################################################### set(ORTHANC_SERVER_SOURCES - OrthancServer/DatabaseWrapper.cpp + OrthancServer/Database/Compatibility/DatabaseLookup.cpp + OrthancServer/Database/Compatibility/ICreateInstance.cpp + OrthancServer/Database/Compatibility/IGetChildrenMetadata.cpp + OrthancServer/Database/Compatibility/ILookupResources.cpp + OrthancServer/Database/Compatibility/SetOfResources.cpp + OrthancServer/Database/ResourcesContent.cpp + OrthancServer/Database/SQLiteDatabaseWrapper.cpp OrthancServer/DicomInstanceOrigin.cpp OrthancServer/DicomInstanceToStore.cpp OrthancServer/ExportedResource.cpp @@ -71,17 +77,11 @@ OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/QueryRetrieveHandler.cpp + OrthancServer/Search/DatabaseConstraint.cpp OrthancServer/Search/DatabaseLookup.cpp OrthancServer/Search/DicomTagConstraint.cpp OrthancServer/Search/HierarchicalMatcher.cpp - OrthancServer/Search/IFindConstraint.cpp - OrthancServer/Search/ListConstraint.cpp - OrthancServer/Search/LookupIdentifierQuery.cpp - OrthancServer/Search/LookupResource.cpp - OrthancServer/Search/RangeConstraint.cpp - OrthancServer/Search/SetOfResources.cpp - OrthancServer/Search/ValueConstraint.cpp - OrthancServer/Search/WildcardConstraint.cpp + OrthancServer/Search/ISqlLookupFormatter.cpp OrthancServer/ServerContext.cpp OrthancServer/ServerEnumerations.cpp OrthancServer/ServerIndex.cpp @@ -91,6 +91,7 @@ OrthancServer/ServerJobs/LuaJobManager.cpp OrthancServer/ServerJobs/MergeStudyJob.cpp OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp + OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.cpp OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp @@ -127,6 +128,8 @@ if (ENABLE_PLUGINS) + include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include) + list(APPEND ORTHANC_SERVER_SOURCES Plugins/Engine/OrthancPluginDatabase.cpp Plugins/Engine/OrthancPlugins.cpp @@ -168,13 +171,16 @@ ##################################################################### set(ORTHANC_EMBEDDED_FILES - PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql - UPGRADE_DATABASE_3_TO_4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql - UPGRADE_DATABASE_4_TO_5 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql - CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json - DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt - LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua - FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json + DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt + FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json + LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua + PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Database/PrepareDatabase.sql + UPGRADE_DATABASE_3_TO_4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Database/Upgrade3To4.sql + UPGRADE_DATABASE_4_TO_5 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Database/Upgrade4To5.sql + + INSTALL_TRACK_ATTACHMENTS_SIZE + ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Database/InstallTrackAttachmentsSize.sql ) if (STANDALONE_BUILD) @@ -231,8 +237,6 @@ endif() -include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include) - add_definitions( -DORTHANC_BUILD_UNIT_TESTS=1 -DORTHANC_ENABLE_LOGGING_PLUGIN=0 diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/Compression/ZipWriter.cpp --- a/Core/Compression/ZipWriter.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/Compression/ZipWriter.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -59,9 +59,9 @@ ptime midnight(today); time_duration sinceMidnight = now - midnight; - zfi.tmz_date.tm_sec = sinceMidnight.seconds(); // seconds after the minute - [0,59] - zfi.tmz_date.tm_min = sinceMidnight.minutes(); // minutes after the hour - [0,59] - zfi.tmz_date.tm_hour = sinceMidnight.hours(); // hours since midnight - [0,23] + zfi.tmz_date.tm_sec = static_cast(sinceMidnight.seconds()); // seconds after the minute - [0,59] + zfi.tmz_date.tm_min = static_cast(sinceMidnight.minutes()); // minutes after the hour - [0,59] + zfi.tmz_date.tm_hour = static_cast(sinceMidnight.hours()); // hours since midnight - [0,23] // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/gregorian/greg_day.html zfi.tmz_date.tm_mday = today.day(); // day of the month - [1,31] diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/DicomNetworking/DicomFindAnswers.cpp --- a/Core/DicomNetworking/DicomFindAnswers.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/DicomNetworking/DicomFindAnswers.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -150,7 +150,35 @@ DcmDataset* DicomFindAnswers::ExtractDcmDataset(size_t index) const { - return new DcmDataset(*GetAnswer(index).GetDcmtkObject().getDataset()); + // As "DicomFindAnswers" stores its content using class + // "ParsedDicomFile" (that internally uses "DcmFileFormat" from + // DCMTK), the dataset can contain tags that are reserved if + // storing the media on the disk, notably tag + // "MediaStorageSOPClassUID" (0002,0002). In this function, we + // remove all those tags whose group is below 0x0008. The + // resulting data set is clean for emission in the C-FIND SCP. + + // http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3 + // https://groups.google.com/d/msg/orthanc-users/D3kpPuX8yV0/_zgHOzkMEQAJ + + DcmDataset& source = *GetAnswer(index).GetDcmtkObject().getDataset(); + + std::auto_ptr target(new DcmDataset); + + for (unsigned long i = 0; i < source.card(); i++) + { + const DcmElement* element = source.getElement(i); + assert(element != NULL); + + if (element != NULL && + element->getTag().getGroup() >= 0x0008 && + element->getTag().getElement() != 0x0000) + { + target->insert(dynamic_cast(element->clone())); + } + } + + return target.release(); } diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/DicomNetworking/DicomUserConnection.cpp --- a/Core/DicomNetworking/DicomUserConnection.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -82,6 +82,10 @@ #include "../PrecompiledHeaders.h" #include "DicomUserConnection.h" +#if !defined(DCMTK_VERSION_NUMBER) +# error The macro DCMTK_VERSION_NUMBER must be defined +#endif + #include "../DicomFormat/DicomArray.h" #include "../Logging.h" #include "../OrthancException.h" @@ -330,7 +334,12 @@ // Figure out which SOP class and SOP instance is encapsulated in the file DIC_UI sopClass; DIC_UI sopInstance; + +#if DCMTK_VERSION_NUMBER >= 364 + if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sizeof(sopClass), sopInstance, sizeof(sopInstance))) +#else if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sopInstance)) +#endif { throw OrthancException(ErrorCode_NoSopClassOrInstance); } @@ -572,7 +581,15 @@ T_DIMSE_C_FindRSP response; DcmDataset* statusDetail = NULL; + +#if DCMTK_VERSION_NUMBER >= 364 + int responseCount; +#endif + OFCondition cond = DIMSE_findUser(association, presID, &request, dataset, +#if DCMTK_VERSION_NUMBER >= 364 + responseCount, +#endif FindCallback, &payload, /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout, diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/DicomNetworking/Internals/CommandDispatcher.cpp --- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -83,6 +83,10 @@ #include "../../PrecompiledHeaders.h" #include "CommandDispatcher.h" +#if !defined(DCMTK_VERSION_NUMBER) +# error The macro DCMTK_VERSION_NUMBER must be defined +#endif + #include "FindScp.h" #include "StoreScp.h" #include "MoveScp.h" @@ -364,7 +368,11 @@ UID_RETIRED_UltrasoundImageStorage, UID_RETIRED_UltrasoundMultiframeImageStorage, UID_RETIRED_VLImageStorage, +#if DCMTK_VERSION_NUMBER >= 364 + UID_RETIRED_VLMultiframeImageStorage, +#else UID_RETIRED_VLMultiFrameImageStorage, +#endif UID_RETIRED_XRayAngiographicBiPlaneImageStorage, // draft UID_DRAFT_SRAudioStorage, @@ -469,8 +477,16 @@ DIC_AE calledAet_C; DIC_AE remoteIp_C; DIC_AE calledIP_C; - if (ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() || - ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad()) + + if ( +#if DCMTK_VERSION_NUMBER >= 364 + ASC_getAPTitles(assoc->params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).bad() || + ASC_getPresentationAddresses(assoc->params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).bad() +#else + ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() || + ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad() +#endif + ) { T_ASC_RejectParameters rej = { @@ -606,7 +622,12 @@ ASC_setAPTitles(assoc->params, NULL, NULL, server.GetApplicationEntityTitle().c_str()); /* acknowledge or reject this association */ +#if DCMTK_VERSION_NUMBER >= 364 + cond = ASC_getApplicationContextName(assoc->params, buf, sizeof(buf)); +#else cond = ASC_getApplicationContextName(assoc->params, buf); +#endif + if ((cond.bad()) || strcmp(buf, UID_StandardApplicationContext) != 0) { /* reject: the application context name is not supported */ diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/DicomNetworking/Internals/FindScp.cpp --- a/Core/DicomNetworking/Internals/FindScp.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/DicomNetworking/Internals/FindScp.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -166,6 +166,28 @@ } + static void FixFindQuery(DicomMap& target, + const DicomMap& source) + { + // "The definition of a Data Set in PS3.5 specifically excludes + // the range of groups below group 0008, and this includes in + // particular Meta Information Header elements such as Transfer + // Syntax UID (0002,0010)." + // http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3 + // https://groups.google.com/d/msg/orthanc-users/D3kpPuX8yV0/_zgHOzkMEQAJ + + DicomArray a(source); + + for (size_t i = 0; i < a.GetSize(); i++) + { + if (a.GetElement(i).GetTag().GetGroup() >= 0x0008) + { + target.SetValue(a.GetElement(i).GetTag(), a.GetElement(i).GetValue()); + } + } + } + + void FindScpCallback( /* in */ @@ -255,7 +277,10 @@ DicomMap input; FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers); - data.findHandler_->Handle(data.answers_, input, sequencesToReturn, + DicomMap filtered; + FixFindQuery(filtered, input); + + data.findHandler_->Handle(data.answers_, filtered, sequencesToReturn, *data.remoteIp_, *data.remoteAet_, *data.calledAet_, modality.GetManufacturer()); ok = true; diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/DicomNetworking/Internals/StoreScp.cpp --- a/Core/DicomNetworking/Internals/StoreScp.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/DicomNetworking/Internals/StoreScp.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -83,6 +83,10 @@ #include "../../PrecompiledHeaders.h" #include "StoreScp.h" +#if !defined(DCMTK_VERSION_NUMBER) +# error The macro DCMTK_VERSION_NUMBER must be defined +#endif + #include "../../DicomParsing/FromDcmtkBridge.h" #include "../../DicomParsing/ToDcmtkBridge.h" #include "../../OrthancException.h" @@ -188,10 +192,16 @@ if (rsp->DimseStatus == STATUS_Success) { // which SOP class and SOP instance ? + +#if DCMTK_VERSION_NUMBER >= 364 + if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sizeof(sopClass), + sopInstance, sizeof(sopInstance), /*opt_correctUIDPadding*/ OFFalse)) +#else if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sopInstance, /*opt_correctUIDPadding*/ OFFalse)) +#endif { - //LOG4CPP_ERROR(Internals::GetLogger(), "bad DICOM file: " << fileName); - rsp->DimseStatus = STATUS_STORE_Error_CannotUnderstand; + //LOG4CPP_ERROR(Internals::GetLogger(), "bad DICOM file: " << fileName); + rsp->DimseStatus = STATUS_STORE_Error_CannotUnderstand; } else if (strcmp(sopClass, req->AffectedSOPClassUID) != 0) { diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/DicomNetworking/TimeoutDicomConnectionManager.cpp --- a/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -97,7 +97,7 @@ unsigned int TimeoutDicomConnectionManager::GetTimeout() { - return timeout_.total_milliseconds(); + return static_cast(timeout_.total_milliseconds()); } diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/DicomParsing/DicomDirWriter.cpp --- a/Core/DicomParsing/DicomDirWriter.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/DicomParsing/DicomDirWriter.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -410,7 +410,14 @@ switch (level) { case ResourceType_Patient: - found = GetUtf8TagValue(id, dataset, encoding, DCM_PatientID); + if (!GetUtf8TagValue(id, dataset, encoding, DCM_PatientID)) + { + // Be tolerant about missing patient ID. Fixes issue #124 + // (GET /studies/ID/media fails for certain dicom file). + id = ""; + } + + found = true; type = ERT_Patient; break; diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/DicomParsing/FromDcmtkBridge.cpp --- a/Core/DicomParsing/FromDcmtkBridge.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -41,6 +41,10 @@ # error The macro ORTHANC_SANDBOXED must be defined #endif +#if !defined(DCMTK_VERSION_NUMBER) +# error The macro DCMTK_VERSION_NUMBER must be defined +#endif + #include "FromDcmtkBridge.h" #include "ToDcmtkBridge.h" #include "../Logging.h" @@ -165,7 +169,11 @@ ~DictionaryLocker() { +#if DCMTK_VERSION_NUMBER >= 364 + dcmDataDict.wrunlock(); +#else dcmDataDict.unlock(); +#endif } DcmDataDictionary& operator*() @@ -2047,7 +2055,7 @@ if (output.type() != Json::objectValue) { throw OrthancException(ErrorCode_LuaBadOutput, - "Lua: IncomingFindRequestFilter must return a table"); + "Lua: The script must return a table"); } Json::Value::Members members = output.getMemberNames(); @@ -2057,7 +2065,7 @@ if (output[members[i]].type() != Json::stringValue) { throw OrthancException(ErrorCode_LuaBadOutput, - "Lua: IncomingFindRequestFilter must return a table " + "Lua: The script must return a table " "mapping names of DICOM tags to strings"); } diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/Enumerations.cpp --- a/Core/Enumerations.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/Enumerations.cpp Thu Jan 24 10:55:19 2019 +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 4cfed5c2eacd -r fc9a4a2dad63 Core/Enumerations.h --- a/Core/Enumerations.h Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/Enumerations.h Thu Jan 24 10:55:19 2019 +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 4cfed5c2eacd -r fc9a4a2dad63 Core/HttpClient.cpp --- a/Core/HttpClient.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/HttpClient.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -62,6 +62,8 @@ } else { + LOG(ERROR) << "Error code " << static_cast(code) + << " in libcurl: " << curl_easy_strerror(code); *status = 0; return code; } @@ -695,8 +697,8 @@ else { answerBody.clear(); - LOG(INFO) << "Error in HTTP request, received HTTP status " << status - << " (" << EnumerationToString(lastStatus_) << ")"; + LOG(ERROR) << "Error in HTTP request, received HTTP status " << status + << " (" << EnumerationToString(lastStatus_) << ")"; } return success; diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/HttpServer/HttpOutput.cpp --- a/Core/HttpServer/HttpOutput.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/HttpServer/HttpOutput.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -407,12 +407,6 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } - if (keepAlive_) - { - throw OrthancException(ErrorCode_NotImplemented, - "Multipart answers are not implemented together with keep-alive connections"); - } - if (state_ != State_WritingHeader) { throw OrthancException(ErrorCode_BadSequenceOfCalls); @@ -428,6 +422,20 @@ std::string header = "HTTP/1.1 200 OK\r\n"; + if (keepAlive_) + { +#if ORTHANC_ENABLE_MONGOOSE == 1 + throw OrthancException(ErrorCode_NotImplemented, + "Multipart answers are not implemented together " + "with keep-alive connections if using Mongoose"); +#else + // Turn off Keep-Alive for multipart answers + // https://github.com/civetweb/civetweb/issues/727 + stream_.DisableKeepAlive(); + header += "Connection: close\r\n"; +#endif + } + // Possibly add the cookies for (std::list::const_iterator it = headers_.begin(); it != headers_.end(); ++it) diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/HttpServer/HttpServer.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/HttpServer.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,1202 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 . + **/ + + +// http://en.highscore.de/cpp/boost/stringhandling.html + +#include "../PrecompiledHeaders.h" +#include "HttpServer.h" + +#include "../Logging.h" +#include "../ChunkedBuffer.h" +#include "../OrthancException.h" +#include "HttpToolbox.h" + +#if ORTHANC_ENABLE_MONGOOSE == 1 +# include + +#elif ORTHANC_ENABLE_CIVETWEB == 1 +# include +# define MONGOOSE_USE_CALLBACKS 1 + +#else +# error "Either Mongoose or Civetweb must be enabled to compile this file" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(ORTHANC_ENABLE_SSL) +# error The macro ORTHANC_ENABLE_SSL must be defined +#endif + +#if ORTHANC_ENABLE_SSL == 1 +#include +#endif + +#define ORTHANC_REALM "Orthanc Secure Area" + + +namespace Orthanc +{ + static const char multipart[] = "multipart/form-data; boundary="; + static unsigned int multipartLength = sizeof(multipart) / sizeof(char) - 1; + + + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + class MongooseOutputStream : public IHttpOutputStream + { + private: + struct mg_connection* connection_; + + public: + MongooseOutputStream(struct mg_connection* connection) : connection_(connection) + { + } + + virtual void Send(bool isHeader, const void* buffer, size_t length) + { + if (length > 0) + { + int status = mg_write(connection_, buffer, length); + if (status != static_cast(length)) + { + // status == 0 when the connection has been closed, -1 on error + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + } + + virtual void OnHttpStatusReceived(HttpStatus status) + { + // Ignore this + } + + virtual void DisableKeepAlive() + { +#if ORTHANC_ENABLE_MONGOOSE == 1 + throw OrthancException(ErrorCode_NotImplemented, + "Only available if using CivetWeb"); +#elif ORTHANC_ENABLE_CIVETWEB == 1 + mg_disable_keep_alive(connection_); +#endif + } + }; + + + enum PostDataStatus + { + PostDataStatus_Success, + PostDataStatus_NoLength, + PostDataStatus_Pending, + PostDataStatus_Failure + }; + } + + +// TODO Move this to external file + + + class ChunkedFile : public ChunkedBuffer + { + private: + std::string filename_; + + public: + ChunkedFile(const std::string& filename) : + filename_(filename) + { + } + + const std::string& GetFilename() const + { + return filename_; + } + }; + + + + class ChunkStore + { + private: + typedef std::list Content; + Content content_; + unsigned int numPlaces_; + + boost::mutex mutex_; + std::set discardedFiles_; + + void Clear() + { + for (Content::iterator it = content_.begin(); + it != content_.end(); ++it) + { + delete *it; + } + } + + Content::iterator Find(const std::string& filename) + { + for (Content::iterator it = content_.begin(); + it != content_.end(); ++it) + { + if ((*it)->GetFilename() == filename) + { + return it; + } + } + + return content_.end(); + } + + void Remove(const std::string& filename) + { + Content::iterator it = Find(filename); + if (it != content_.end()) + { + delete *it; + content_.erase(it); + } + } + + public: + ChunkStore() + { + numPlaces_ = 10; + } + + ~ChunkStore() + { + Clear(); + } + + PostDataStatus Store(std::string& completed, + const char* chunkData, + size_t chunkSize, + const std::string& filename, + size_t filesize) + { + boost::mutex::scoped_lock lock(mutex_); + + std::set::iterator wasDiscarded = discardedFiles_.find(filename); + if (wasDiscarded != discardedFiles_.end()) + { + discardedFiles_.erase(wasDiscarded); + return PostDataStatus_Failure; + } + + ChunkedFile* f; + Content::iterator it = Find(filename); + if (it == content_.end()) + { + f = new ChunkedFile(filename); + + // Make some room + if (content_.size() >= numPlaces_) + { + discardedFiles_.insert(content_.front()->GetFilename()); + delete content_.front(); + content_.pop_front(); + } + + content_.push_back(f); + } + else + { + f = *it; + } + + f->AddChunk(chunkData, chunkSize); + + if (f->GetNumBytes() > filesize) + { + Remove(filename); + } + else if (f->GetNumBytes() == filesize) + { + f->Flatten(completed); + Remove(filename); + return PostDataStatus_Success; + } + + return PostDataStatus_Pending; + } + + /*void Print() + { + boost::mutex::scoped_lock lock(mutex_); + + printf("ChunkStore status:\n"); + for (Content::const_iterator i = content_.begin(); + i != content_.end(); i++) + { + printf(" [%s]: %d\n", (*i)->GetFilename().c_str(), (*i)->GetNumBytes()); + } + printf("-----\n"); + }*/ + }; + + + struct HttpServer::PImpl + { + struct mg_context *context_; + ChunkStore chunkStore_; + }; + + + ChunkStore& HttpServer::GetChunkStore() + { + return pimpl_->chunkStore_; + } + + + + static PostDataStatus ReadBody(std::string& postData, + struct mg_connection *connection, + const IHttpHandler::Arguments& headers) + { + IHttpHandler::Arguments::const_iterator cs = headers.find("content-length"); + if (cs == headers.end()) + { + return PostDataStatus_NoLength; + } + + int length; + try + { + length = boost::lexical_cast(cs->second); + } + catch (boost::bad_lexical_cast&) + { + return PostDataStatus_NoLength; + } + + if (length < 0) + { + length = 0; + } + + postData.resize(length); + + size_t pos = 0; + while (length > 0) + { + int r = mg_read(connection, &postData[pos], length); + if (r <= 0) + { + return PostDataStatus_Failure; + } + + assert(r <= length); + length -= r; + pos += r; + } + + return PostDataStatus_Success; + } + + + + static PostDataStatus ParseMultipartPost(std::string &completedFile, + struct mg_connection *connection, + const IHttpHandler::Arguments& headers, + const std::string& contentType, + ChunkStore& chunkStore) + { + std::string boundary = "--" + contentType.substr(multipartLength); + + std::string postData; + PostDataStatus status = ReadBody(postData, connection, headers); + + if (status != PostDataStatus_Success) + { + return status; + } + + /*for (IHttpHandler::Arguments::const_iterator i = headers.begin(); i != headers.end(); i++) + { + std::cout << "Header [" << i->first << "] = " << i->second << "\n"; + } + printf("CHUNK\n");*/ + + typedef IHttpHandler::Arguments::const_iterator ArgumentIterator; + + ArgumentIterator requestedWith = headers.find("x-requested-with"); + ArgumentIterator fileName = headers.find("x-file-name"); + ArgumentIterator fileSizeStr = headers.find("x-file-size"); + + if (requestedWith != headers.end() && + requestedWith->second != "XMLHttpRequest") + { + return PostDataStatus_Failure; + } + + size_t fileSize = 0; + if (fileSizeStr != headers.end()) + { + try + { + fileSize = boost::lexical_cast(fileSizeStr->second); + } + catch (boost::bad_lexical_cast&) + { + return PostDataStatus_Failure; + } + } + + typedef boost::find_iterator FindIterator; + typedef boost::iterator_range Range; + + //chunkStore.Print(); + + try + { + FindIterator last; + for (FindIterator it = + make_find_iterator(postData, boost::first_finder(boundary)); + it!=FindIterator(); + ++it) + { + if (last != FindIterator()) + { + Range part(&last->back(), &it->front()); + Range content = boost::find_first(part, "\r\n\r\n"); + if (/*content != Range()*/!content.empty()) + { + Range c(&content.back() + 1, &it->front() - 2); + size_t chunkSize = c.size(); + + if (chunkSize > 0) + { + const char* chunkData = &c.front(); + + if (fileName == headers.end()) + { + // This file is stored in a single chunk + completedFile.resize(chunkSize); + if (chunkSize > 0) + { + memcpy(&completedFile[0], chunkData, chunkSize); + } + return PostDataStatus_Success; + } + else + { + return chunkStore.Store(completedFile, chunkData, chunkSize, fileName->second, fileSize); + } + } + } + } + + last = it; + } + } + catch (std::length_error&) + { + return PostDataStatus_Failure; + } + + return PostDataStatus_Pending; + } + + + static bool IsAccessGranted(const HttpServer& that, + const IHttpHandler::Arguments& headers) + { + bool granted = false; + + IHttpHandler::Arguments::const_iterator auth = headers.find("authorization"); + if (auth != headers.end()) + { + std::string s = auth->second; + if (s.size() > 6 && + s.substr(0, 6) == "Basic ") + { + std::string b64 = s.substr(6); + granted = that.IsValidBasicHttpAuthentication(b64); + } + } + + return granted; + } + + + static std::string GetAuthenticatedUsername(const IHttpHandler::Arguments& headers) + { + IHttpHandler::Arguments::const_iterator auth = headers.find("authorization"); + + if (auth == headers.end()) + { + return ""; + } + + std::string s = auth->second; + if (s.size() <= 6 || + s.substr(0, 6) != "Basic ") + { + return ""; + } + + std::string b64 = s.substr(6); + std::string decoded; + Toolbox::DecodeBase64(decoded, b64); + size_t semicolons = decoded.find(':'); + + if (semicolons == std::string::npos) + { + // Bad-formatted request + return ""; + } + else + { + return decoded.substr(0, semicolons); + } + } + + + static bool ExtractMethod(HttpMethod& method, + const struct mg_request_info *request, + const IHttpHandler::Arguments& headers, + const IHttpHandler::GetArguments& argumentsGET) + { + std::string overriden; + + // Check whether some PUT/DELETE faking is done + + // 1. Faking with Google's approach + IHttpHandler::Arguments::const_iterator methodOverride = + headers.find("x-http-method-override"); + + if (methodOverride != headers.end()) + { + overriden = methodOverride->second; + } + else if (!strcmp(request->request_method, "GET")) + { + // 2. Faking with Ruby on Rail's approach + // GET /my/resource?_method=delete <=> DELETE /my/resource + for (size_t i = 0; i < argumentsGET.size(); i++) + { + if (argumentsGET[i].first == "_method") + { + overriden = argumentsGET[i].second; + break; + } + } + } + + if (overriden.size() > 0) + { + // A faking has been done within this request + Toolbox::ToUpperCase(overriden); + + LOG(INFO) << "HTTP method faking has been detected for " << overriden; + + if (overriden == "PUT") + { + method = HttpMethod_Put; + return true; + } + else if (overriden == "DELETE") + { + method = HttpMethod_Delete; + return true; + } + else + { + return false; + } + } + + // No PUT/DELETE faking was present + if (!strcmp(request->request_method, "GET")) + { + method = HttpMethod_Get; + } + else if (!strcmp(request->request_method, "POST")) + { + method = HttpMethod_Post; + } + else if (!strcmp(request->request_method, "DELETE")) + { + method = HttpMethod_Delete; + } + else if (!strcmp(request->request_method, "PUT")) + { + method = HttpMethod_Put; + } + else + { + return false; + } + + return true; + } + + + static void ConfigureHttpCompression(HttpOutput& output, + const IHttpHandler::Arguments& headers) + { + // Look if the client wishes HTTP compression + // https://en.wikipedia.org/wiki/HTTP_compression + IHttpHandler::Arguments::const_iterator it = headers.find("accept-encoding"); + if (it != headers.end()) + { + std::vector encodings; + Toolbox::TokenizeString(encodings, it->second, ','); + + for (size_t i = 0; i < encodings.size(); i++) + { + std::string s = Toolbox::StripSpaces(encodings[i]); + + if (s == "deflate") + { + output.SetDeflateAllowed(true); + } + else if (s == "gzip") + { + output.SetGzipAllowed(true); + } + } + } + } + + + static void InternalCallback(HttpOutput& output /* out */, + HttpMethod& method /* out */, + HttpServer& server, + struct mg_connection *connection, + const struct mg_request_info *request) + { + bool localhost; + +#if ORTHANC_ENABLE_MONGOOSE == 1 + static const long LOCALHOST = (127ll << 24) + 1ll; + localhost = (request->remote_ip == LOCALHOST); +#elif ORTHANC_ENABLE_CIVETWEB == 1 + // The "remote_ip" field of "struct mg_request_info" is tagged as + // deprecated in Civetweb, using "remote_addr" instead. + localhost = (std::string(request->remote_addr) == "127.0.0.1"); +#else +# error +#endif + + // Check remote calls + if (!server.IsRemoteAccessAllowed() && + !localhost) + { + output.SendUnauthorized(server.GetRealm()); + return; + } + + + // Extract the HTTP headers + IHttpHandler::Arguments headers; + for (int i = 0; i < request->num_headers; i++) + { + std::string name = request->http_headers[i].name; + std::string value = request->http_headers[i].value; + + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + headers.insert(std::make_pair(name, value)); + VLOG(1) << "HTTP header: [" << name << "]: [" << value << "]"; + } + + if (server.IsHttpCompressionEnabled()) + { + ConfigureHttpCompression(output, headers); + } + + + // Extract the GET arguments + IHttpHandler::GetArguments argumentsGET; + if (!strcmp(request->request_method, "GET")) + { + HttpToolbox::ParseGetArguments(argumentsGET, request->query_string); + } + + + // Compute the HTTP method, taking method faking into consideration + method = HttpMethod_Get; + if (!ExtractMethod(method, request, headers, argumentsGET)) + { + output.SendStatus(HttpStatus_400_BadRequest); + return; + } + + + // Authenticate this connection + if (server.IsAuthenticationEnabled() && + !IsAccessGranted(server, headers)) + { + output.SendUnauthorized(server.GetRealm()); + return; + } + + +#if ORTHANC_ENABLE_MONGOOSE == 1 + // Apply the filter, if it is installed + char remoteIp[24]; + sprintf(remoteIp, "%d.%d.%d.%d", + reinterpret_cast(&request->remote_ip) [3], + reinterpret_cast(&request->remote_ip) [2], + reinterpret_cast(&request->remote_ip) [1], + reinterpret_cast(&request->remote_ip) [0]); + + const char* requestUri = request->uri; + +#elif ORTHANC_ENABLE_CIVETWEB == 1 + const char* remoteIp = request->remote_addr; + const char* requestUri = request->local_uri; +#else +# error +#endif + + if (requestUri == NULL) + { + requestUri = ""; + } + + std::string username = GetAuthenticatedUsername(headers); + + IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); + if (filter != NULL) + { + if (!filter->IsAllowed(method, requestUri, remoteIp, + username.c_str(), headers, argumentsGET)) + { + //output.SendUnauthorized(server.GetRealm()); + output.SendStatus(HttpStatus_403_Forbidden); + return; + } + } + + + // Extract the body of the request for PUT and POST + + // TODO Avoid unneccessary memcopy of the body + + std::string body; + if (method == HttpMethod_Post || + method == HttpMethod_Put) + { + PostDataStatus status; + + IHttpHandler::Arguments::const_iterator ct = headers.find("content-type"); + if (ct == headers.end()) + { + // No content-type specified. Assume no multi-part content occurs at this point. + status = ReadBody(body, connection, headers); + } + else + { + std::string contentType = ct->second; + if (contentType.size() >= multipartLength && + !memcmp(contentType.c_str(), multipart, multipartLength)) + { + status = ParseMultipartPost(body, connection, headers, contentType, server.GetChunkStore()); + } + else + { + status = ReadBody(body, connection, headers); + } + } + + switch (status) + { + case PostDataStatus_NoLength: + output.SendStatus(HttpStatus_411_LengthRequired); + return; + + case PostDataStatus_Failure: + output.SendStatus(HttpStatus_400_BadRequest); + return; + + case PostDataStatus_Pending: + output.AnswerEmpty(); + return; + + default: + break; + } + } + + + // Decompose the URI into its components + UriComponents uri; + try + { + Toolbox::SplitUriComponents(uri, requestUri); + } + catch (OrthancException&) + { + output.SendStatus(HttpStatus_400_BadRequest); + return; + } + + + LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri); + + bool found = false; + + if (server.HasHandler()) + { + found = server.GetHandler().Handle(output, RequestOrigin_RestApi, remoteIp, username.c_str(), + method, uri, headers, argumentsGET, body.c_str(), body.size()); + } + + if (!found) + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + + static void ProtectedCallback(struct mg_connection *connection, + const struct mg_request_info *request) + { + try + { +#if ORTHANC_ENABLE_MONGOOSE == 1 + void *that = request->user_data; + const char* requestUri = request->uri; +#elif ORTHANC_ENABLE_CIVETWEB == 1 + // https://github.com/civetweb/civetweb/issues/409 + void *that = mg_get_user_data(mg_get_context(connection)); + const char* requestUri = request->local_uri; +#else +# error +#endif + + if (requestUri == NULL) + { + requestUri = ""; + } + + HttpServer* server = reinterpret_cast(that); + + if (server == NULL) + { + MongooseOutputStream stream(connection); + HttpOutput output(stream, false /* assume no keep-alive */); + output.SendStatus(HttpStatus_500_InternalServerError); + return; + } + + MongooseOutputStream stream(connection); + HttpOutput output(stream, server->IsKeepAliveEnabled()); + HttpMethod method = HttpMethod_Get; + + try + { + try + { + InternalCallback(output, method, *server, connection, request); + } + catch (OrthancException&) + { + throw; // Pass the exception to the main handler below + } + // Now convert native exceptions as OrthancException + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_BadParameterType, + "Syntax error in some user-supplied data"); + } + catch (std::runtime_error&) + { + // Presumably an error while parsing the JSON body + throw OrthancException(ErrorCode_BadRequest); + } + catch (std::bad_alloc&) + { + throw OrthancException(ErrorCode_NotEnoughMemory, + "The server hosting Orthanc is running out of memory"); + } + catch (...) + { + throw OrthancException(ErrorCode_InternalError, + "An unhandled exception was generated inside the HTTP server"); + } + } + catch (OrthancException& e) + { + assert(server != NULL); + + // Using this candidate handler results in an exception + try + { + if (server->GetExceptionFormatter() == NULL) + { + LOG(ERROR) << "Exception in the HTTP handler: " << e.What(); + output.SendStatus(e.GetHttpStatus()); + } + else + { + server->GetExceptionFormatter()->Format(output, e, method, requestUri); + } + } + catch (OrthancException&) + { + // An exception here reflects the fact that the status code + // was already set by the HTTP handler. + } + } + } + catch (...) + { + // We should never arrive at this point, where it is even impossible to send an answer + LOG(ERROR) << "Catastrophic error inside the HTTP server, giving up"; + } + } + + +#if MONGOOSE_USE_CALLBACKS == 0 + static void* Callback(enum mg_event event, + struct mg_connection *connection, + const struct mg_request_info *request) + { + if (event == MG_NEW_REQUEST) + { + ProtectedCallback(connection, request); + + // Mark as processed + return (void*) ""; + } + else + { + return NULL; + } + } + +#elif MONGOOSE_USE_CALLBACKS == 1 + static int Callback(struct mg_connection *connection) + { + const struct mg_request_info *request = mg_get_request_info(connection); + + ProtectedCallback(connection, request); + + return 1; // Do not let Mongoose handle the request by itself + } + +#else +# error Please set MONGOOSE_USE_CALLBACKS +#endif + + + + + + bool HttpServer::IsRunning() const + { + return (pimpl_->context_ != NULL); + } + + + HttpServer::HttpServer() : pimpl_(new PImpl) + { + pimpl_->context_ = NULL; + handler_ = NULL; + remoteAllowed_ = false; + authentication_ = false; + ssl_ = false; + port_ = 8000; + filter_ = NULL; + keepAlive_ = false; + httpCompression_ = true; + exceptionFormatter_ = NULL; + realm_ = ORTHANC_REALM; + threadsCount_ = 50; // Default value in mongoose + tcpNoDelay_ = true; + +#if ORTHANC_ENABLE_MONGOOSE == 1 + LOG(INFO) << "This Orthanc server uses Mongoose as its embedded HTTP server"; +#endif + +#if ORTHANC_ENABLE_CIVETWEB == 1 + LOG(INFO) << "This Orthanc server uses CivetWeb as its embedded HTTP server"; +#endif + +#if ORTHANC_ENABLE_SSL == 1 + // Check for the Heartbleed exploit + // https://en.wikipedia.org/wiki/OpenSSL#Heartbleed_bug + if (OPENSSL_VERSION_NUMBER < 0x1000107fL /* openssl-1.0.1g */ && + OPENSSL_VERSION_NUMBER >= 0x1000100fL /* openssl-1.0.1 */) + { + LOG(WARNING) << "This version of OpenSSL is vulnerable to the Heartbleed exploit"; + } +#endif + } + + + HttpServer::~HttpServer() + { + Stop(); + } + + + void HttpServer::SetPortNumber(uint16_t port) + { + Stop(); + port_ = port; + } + + void HttpServer::Start() + { +#if ORTHANC_ENABLE_MONGOOSE == 1 + LOG(INFO) << "Starting embedded Web server using Mongoose"; +#elif ORTHANC_ENABLE_CIVETWEB == 1 + LOG(INFO) << "Starting embedded Web server using Civetweb"; +#else +# error +#endif + + if (!IsRunning()) + { + std::string port = boost::lexical_cast(port_); + std::string numThreads = boost::lexical_cast(threadsCount_); + + if (ssl_) + { + port += "s"; + } + + const char *options[] = { + // Set the TCP port for the HTTP server + "listening_ports", port.c_str(), + + // Optimization reported by Chris Hafey + // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ + "enable_keep_alive", (keepAlive_ ? "yes" : "no"), + +#if ORTHANC_ENABLE_CIVETWEB == 1 + // https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#enable_keep_alive-no + "keep_alive_timeout_ms", (keepAlive_ ? "500" : "0"), +#endif + +#if ORTHANC_ENABLE_CIVETWEB == 1 + // Disable TCP Nagle's algorithm to maximize speed (this + // option is not available in Mongoose). + // https://groups.google.com/d/topic/civetweb/35HBR9seFjU/discussion + // https://eklitzke.org/the-caveats-of-tcp-nodelay + "tcp_nodelay", (tcpNoDelay_ ? "1" : "0"), +#endif + + // Set the number of threads + "num_threads", numThreads.c_str(), + + // Set the SSL certificate, if any. This must be the last option. + ssl_ ? "ssl_certificate" : NULL, + certificate_.c_str(), + NULL + }; + +#if MONGOOSE_USE_CALLBACKS == 0 + pimpl_->context_ = mg_start(&Callback, this, options); + +#elif MONGOOSE_USE_CALLBACKS == 1 + struct mg_callbacks callbacks; + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.begin_request = Callback; + pimpl_->context_ = mg_start(&callbacks, this, options); + +#else +# error Please set MONGOOSE_USE_CALLBACKS +#endif + + if (!pimpl_->context_) + { + throw OrthancException(ErrorCode_HttpPortInUse); + } + + LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber() + << " (HTTPS encryption is " + << (IsSslEnabled() ? "enabled" : "disabled") + << ", remote access is " + << (IsRemoteAccessAllowed() ? "" : "not ") + << "allowed)"; + } + } + + void HttpServer::Stop() + { + if (IsRunning()) + { + mg_stop(pimpl_->context_); + pimpl_->context_ = NULL; + } + } + + + void HttpServer::ClearUsers() + { + Stop(); + registeredUsers_.clear(); + } + + + void HttpServer::RegisterUser(const char* username, + const char* password) + { + Stop(); + + std::string tag = std::string(username) + ":" + std::string(password); + std::string encoded; + Toolbox::EncodeBase64(encoded, tag); + registeredUsers_.insert(encoded); + } + + void HttpServer::SetSslEnabled(bool enabled) + { + Stop(); + +#if ORTHANC_ENABLE_SSL == 0 + if (enabled) + { + throw OrthancException(ErrorCode_SslDisabled); + } + else + { + ssl_ = false; + } +#else + ssl_ = enabled; +#endif + } + + + void HttpServer::SetKeepAliveEnabled(bool enabled) + { + Stop(); + keepAlive_ = enabled; + LOG(INFO) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled"); + +#if ORTHANC_ENABLE_MONGOOSE == 1 + if (enabled) + { + LOG(WARNING) << "You should disable HTTP keep alive, as you are using Mongoose"; + } +#endif + } + + + void HttpServer::SetAuthenticationEnabled(bool enabled) + { + Stop(); + authentication_ = enabled; + } + + void HttpServer::SetSslCertificate(const char* path) + { + Stop(); + certificate_ = path; + } + + void HttpServer::SetRemoteAccessAllowed(bool allowed) + { + Stop(); + remoteAllowed_ = allowed; + } + + void HttpServer::SetHttpCompressionEnabled(bool enabled) + { + Stop(); + httpCompression_ = enabled; + LOG(WARNING) << "HTTP compression is " << (enabled ? "enabled" : "disabled"); + } + + void HttpServer::SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter) + { + Stop(); + filter_ = &filter; + } + + + void HttpServer::SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter) + { + Stop(); + exceptionFormatter_ = &formatter; + } + + + bool HttpServer::IsValidBasicHttpAuthentication(const std::string& basic) const + { + return registeredUsers_.find(basic) != registeredUsers_.end(); + } + + + void HttpServer::Register(IHttpHandler& handler) + { + Stop(); + handler_ = &handler; + } + + + IHttpHandler& HttpServer::GetHandler() const + { + if (handler_ == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + return *handler_; + } + + + void HttpServer::SetThreadsCount(unsigned int threads) + { + if (threads <= 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + Stop(); + threadsCount_ = threads; + } + + + void HttpServer::SetTcpNoDelay(bool tcpNoDelay) + { + Stop(); + tcpNoDelay_ = tcpNoDelay; + LOG(INFO) << "TCP_NODELAY for the HTTP sockets is set to " + << (tcpNoDelay ? "true" : "false"); + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/HttpServer/HttpServer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/HttpServer.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,219 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 + +#if !defined(ORTHANC_ENABLE_MONGOOSE) +# error Macro ORTHANC_ENABLE_MONGOOSE must be defined to include this file +#endif + +#if !defined(ORTHANC_ENABLE_CIVETWEB) +# error Macro ORTHANC_ENABLE_CIVETWEB must be defined to include this file +#endif + +#if (ORTHANC_ENABLE_MONGOOSE == 0 && \ + ORTHANC_ENABLE_CIVETWEB == 0) +# error Either ORTHANC_ENABLE_MONGOOSE or ORTHANC_ENABLE_CIVETWEB must be set to 1 +#endif + + +#include "IIncomingHttpRequestFilter.h" + +#include +#include +#include +#include +#include + +namespace Orthanc +{ + class ChunkStore; + class OrthancException; + + class IHttpExceptionFormatter : public boost::noncopyable + { + public: + virtual ~IHttpExceptionFormatter() + { + } + + virtual void Format(HttpOutput& output, + const OrthancException& exception, + HttpMethod method, + const char* uri) = 0; + }; + + + class HttpServer + { + private: + // http://stackoverflow.com/questions/311166/stdauto-ptr-or-boostshared-ptr-for-pimpl-idiom + struct PImpl; + boost::shared_ptr pimpl_; + + IHttpHandler *handler_; + + typedef std::set RegisteredUsers; + RegisteredUsers registeredUsers_; + + bool remoteAllowed_; + bool authentication_; + bool ssl_; + std::string certificate_; + uint16_t port_; + IIncomingHttpRequestFilter* filter_; + bool keepAlive_; + bool httpCompression_; + IHttpExceptionFormatter* exceptionFormatter_; + std::string realm_; + unsigned int threadsCount_; + bool tcpNoDelay_; + + bool IsRunning() const; + + public: + HttpServer(); + + ~HttpServer(); + + void SetPortNumber(uint16_t port); + + uint16_t GetPortNumber() const + { + return port_; + } + + void Start(); + + void Stop(); + + void ClearUsers(); + + void RegisterUser(const char* username, + const char* password); + + bool IsAuthenticationEnabled() const + { + return authentication_; + } + + void SetAuthenticationEnabled(bool enabled); + + bool IsSslEnabled() const + { + return ssl_; + } + + void SetSslEnabled(bool enabled); + + bool IsKeepAliveEnabled() const + { + return keepAlive_; + } + + void SetKeepAliveEnabled(bool enabled); + + const std::string& GetSslCertificate() const + { + return certificate_; + } + + void SetSslCertificate(const char* path); + + bool IsRemoteAccessAllowed() const + { + return remoteAllowed_; + } + + void SetRemoteAccessAllowed(bool allowed); + + bool IsHttpCompressionEnabled() const + { + return httpCompression_;; + } + + void SetHttpCompressionEnabled(bool enabled); + + IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const + { + return filter_; + } + + void SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter); + + ChunkStore& GetChunkStore(); + + bool IsValidBasicHttpAuthentication(const std::string& basic) const; + + void Register(IHttpHandler& handler); + + bool HasHandler() const + { + return handler_ != NULL; + } + + IHttpHandler& GetHandler() const; + + void SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter); + + IHttpExceptionFormatter* GetExceptionFormatter() + { + return exceptionFormatter_; + } + + const std::string& GetRealm() const + { + return realm_; + } + + void SetRealm(const std::string& realm) + { + realm_ = realm; + } + + void SetThreadsCount(unsigned int threads); + + unsigned int GetThreadsCount() const + { + return threadsCount_; + } + + // New in Orthanc 1.5.2, not available for Mongoose + void SetTcpNoDelay(bool tcpNoDelay); + + bool IsTcpNoDelay() const + { + return tcpNoDelay_; + } + }; +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/HttpServer/IHttpOutputStream.h --- a/Core/HttpServer/IHttpOutputStream.h Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/HttpServer/IHttpOutputStream.h Thu Jan 24 10:55:19 2019 +0100 @@ -50,5 +50,9 @@ virtual void OnHttpStatusReceived(HttpStatus status) = 0; virtual void Send(bool isHeader, const void* buffer, size_t length) = 0; + + // Disable HTTP keep alive for this single HTTP connection. Must + // be called before sending the "HTTP/1.1 200 OK" header. + virtual void DisableKeepAlive() = 0; }; } diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/HttpServer/MongooseServer.cpp --- a/Core/HttpServer/MongooseServer.cpp Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1159 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 . - **/ - - -// http://en.highscore.de/cpp/boost/stringhandling.html - -#include "../PrecompiledHeaders.h" -#include "MongooseServer.h" - -#include "../Logging.h" -#include "../ChunkedBuffer.h" -#include "../OrthancException.h" -#include "HttpToolbox.h" - -#if ORTHANC_ENABLE_MONGOOSE == 1 -# include - -#elif ORTHANC_ENABLE_CIVETWEB == 1 -# include -# define MONGOOSE_USE_CALLBACKS 1 - -#else -# error "Either Mongoose or Civetweb must be enabled to compile this file" -#endif - -#include -#include -#include -#include -#include -#include -#include -#include - -#if !defined(ORTHANC_ENABLE_SSL) -# error The macro ORTHANC_ENABLE_SSL must be defined -#endif - -#if ORTHANC_ENABLE_SSL == 1 -#include -#endif - -#define ORTHANC_REALM "Orthanc Secure Area" - - -namespace Orthanc -{ - static const char multipart[] = "multipart/form-data; boundary="; - static unsigned int multipartLength = sizeof(multipart) / sizeof(char) - 1; - - - namespace - { - // Anonymous namespace to avoid clashes between compilation modules - class MongooseOutputStream : public IHttpOutputStream - { - private: - struct mg_connection* connection_; - - public: - MongooseOutputStream(struct mg_connection* connection) : connection_(connection) - { - } - - virtual void Send(bool isHeader, const void* buffer, size_t length) - { - if (length > 0) - { - int status = mg_write(connection_, buffer, length); - if (status != static_cast(length)) - { - // status == 0 when the connection has been closed, -1 on error - throw OrthancException(ErrorCode_NetworkProtocol); - } - } - } - - virtual void OnHttpStatusReceived(HttpStatus status) - { - // Ignore this - } - }; - - - enum PostDataStatus - { - PostDataStatus_Success, - PostDataStatus_NoLength, - PostDataStatus_Pending, - PostDataStatus_Failure - }; - } - - -// TODO Move this to external file - - - class ChunkedFile : public ChunkedBuffer - { - private: - std::string filename_; - - public: - ChunkedFile(const std::string& filename) : - filename_(filename) - { - } - - const std::string& GetFilename() const - { - return filename_; - } - }; - - - - class ChunkStore - { - private: - typedef std::list Content; - Content content_; - unsigned int numPlaces_; - - boost::mutex mutex_; - std::set discardedFiles_; - - void Clear() - { - for (Content::iterator it = content_.begin(); - it != content_.end(); ++it) - { - delete *it; - } - } - - Content::iterator Find(const std::string& filename) - { - for (Content::iterator it = content_.begin(); - it != content_.end(); ++it) - { - if ((*it)->GetFilename() == filename) - { - return it; - } - } - - return content_.end(); - } - - void Remove(const std::string& filename) - { - Content::iterator it = Find(filename); - if (it != content_.end()) - { - delete *it; - content_.erase(it); - } - } - - public: - ChunkStore() - { - numPlaces_ = 10; - } - - ~ChunkStore() - { - Clear(); - } - - PostDataStatus Store(std::string& completed, - const char* chunkData, - size_t chunkSize, - const std::string& filename, - size_t filesize) - { - boost::mutex::scoped_lock lock(mutex_); - - std::set::iterator wasDiscarded = discardedFiles_.find(filename); - if (wasDiscarded != discardedFiles_.end()) - { - discardedFiles_.erase(wasDiscarded); - return PostDataStatus_Failure; - } - - ChunkedFile* f; - Content::iterator it = Find(filename); - if (it == content_.end()) - { - f = new ChunkedFile(filename); - - // Make some room - if (content_.size() >= numPlaces_) - { - discardedFiles_.insert(content_.front()->GetFilename()); - delete content_.front(); - content_.pop_front(); - } - - content_.push_back(f); - } - else - { - f = *it; - } - - f->AddChunk(chunkData, chunkSize); - - if (f->GetNumBytes() > filesize) - { - Remove(filename); - } - else if (f->GetNumBytes() == filesize) - { - f->Flatten(completed); - Remove(filename); - return PostDataStatus_Success; - } - - return PostDataStatus_Pending; - } - - /*void Print() - { - boost::mutex::scoped_lock lock(mutex_); - - printf("ChunkStore status:\n"); - for (Content::const_iterator i = content_.begin(); - i != content_.end(); i++) - { - printf(" [%s]: %d\n", (*i)->GetFilename().c_str(), (*i)->GetNumBytes()); - } - printf("-----\n"); - }*/ - }; - - - struct MongooseServer::PImpl - { - struct mg_context *context_; - ChunkStore chunkStore_; - }; - - - ChunkStore& MongooseServer::GetChunkStore() - { - return pimpl_->chunkStore_; - } - - - - static PostDataStatus ReadBody(std::string& postData, - struct mg_connection *connection, - const IHttpHandler::Arguments& headers) - { - IHttpHandler::Arguments::const_iterator cs = headers.find("content-length"); - if (cs == headers.end()) - { - return PostDataStatus_NoLength; - } - - int length; - try - { - length = boost::lexical_cast(cs->second); - } - catch (boost::bad_lexical_cast&) - { - return PostDataStatus_NoLength; - } - - if (length < 0) - { - length = 0; - } - - postData.resize(length); - - size_t pos = 0; - while (length > 0) - { - int r = mg_read(connection, &postData[pos], length); - if (r <= 0) - { - return PostDataStatus_Failure; - } - - assert(r <= length); - length -= r; - pos += r; - } - - return PostDataStatus_Success; - } - - - - static PostDataStatus ParseMultipartPost(std::string &completedFile, - struct mg_connection *connection, - const IHttpHandler::Arguments& headers, - const std::string& contentType, - ChunkStore& chunkStore) - { - std::string boundary = "--" + contentType.substr(multipartLength); - - std::string postData; - PostDataStatus status = ReadBody(postData, connection, headers); - - if (status != PostDataStatus_Success) - { - return status; - } - - /*for (IHttpHandler::Arguments::const_iterator i = headers.begin(); i != headers.end(); i++) - { - std::cout << "Header [" << i->first << "] = " << i->second << "\n"; - } - printf("CHUNK\n");*/ - - typedef IHttpHandler::Arguments::const_iterator ArgumentIterator; - - ArgumentIterator requestedWith = headers.find("x-requested-with"); - ArgumentIterator fileName = headers.find("x-file-name"); - ArgumentIterator fileSizeStr = headers.find("x-file-size"); - - if (requestedWith != headers.end() && - requestedWith->second != "XMLHttpRequest") - { - return PostDataStatus_Failure; - } - - size_t fileSize = 0; - if (fileSizeStr != headers.end()) - { - try - { - fileSize = boost::lexical_cast(fileSizeStr->second); - } - catch (boost::bad_lexical_cast&) - { - return PostDataStatus_Failure; - } - } - - typedef boost::find_iterator FindIterator; - typedef boost::iterator_range Range; - - //chunkStore.Print(); - - try - { - FindIterator last; - for (FindIterator it = - make_find_iterator(postData, boost::first_finder(boundary)); - it!=FindIterator(); - ++it) - { - if (last != FindIterator()) - { - Range part(&last->back(), &it->front()); - Range content = boost::find_first(part, "\r\n\r\n"); - if (/*content != Range()*/!content.empty()) - { - Range c(&content.back() + 1, &it->front() - 2); - size_t chunkSize = c.size(); - - if (chunkSize > 0) - { - const char* chunkData = &c.front(); - - if (fileName == headers.end()) - { - // This file is stored in a single chunk - completedFile.resize(chunkSize); - if (chunkSize > 0) - { - memcpy(&completedFile[0], chunkData, chunkSize); - } - return PostDataStatus_Success; - } - else - { - return chunkStore.Store(completedFile, chunkData, chunkSize, fileName->second, fileSize); - } - } - } - } - - last = it; - } - } - catch (std::length_error&) - { - return PostDataStatus_Failure; - } - - return PostDataStatus_Pending; - } - - - static bool IsAccessGranted(const MongooseServer& that, - const IHttpHandler::Arguments& headers) - { - bool granted = false; - - IHttpHandler::Arguments::const_iterator auth = headers.find("authorization"); - if (auth != headers.end()) - { - std::string s = auth->second; - if (s.size() > 6 && - s.substr(0, 6) == "Basic ") - { - std::string b64 = s.substr(6); - granted = that.IsValidBasicHttpAuthentication(b64); - } - } - - return granted; - } - - - static std::string GetAuthenticatedUsername(const IHttpHandler::Arguments& headers) - { - IHttpHandler::Arguments::const_iterator auth = headers.find("authorization"); - - if (auth == headers.end()) - { - return ""; - } - - std::string s = auth->second; - if (s.size() <= 6 || - s.substr(0, 6) != "Basic ") - { - return ""; - } - - std::string b64 = s.substr(6); - std::string decoded; - Toolbox::DecodeBase64(decoded, b64); - size_t semicolons = decoded.find(':'); - - if (semicolons == std::string::npos) - { - // Bad-formatted request - return ""; - } - else - { - return decoded.substr(0, semicolons); - } - } - - - static bool ExtractMethod(HttpMethod& method, - const struct mg_request_info *request, - const IHttpHandler::Arguments& headers, - const IHttpHandler::GetArguments& argumentsGET) - { - std::string overriden; - - // Check whether some PUT/DELETE faking is done - - // 1. Faking with Google's approach - IHttpHandler::Arguments::const_iterator methodOverride = - headers.find("x-http-method-override"); - - if (methodOverride != headers.end()) - { - overriden = methodOverride->second; - } - else if (!strcmp(request->request_method, "GET")) - { - // 2. Faking with Ruby on Rail's approach - // GET /my/resource?_method=delete <=> DELETE /my/resource - for (size_t i = 0; i < argumentsGET.size(); i++) - { - if (argumentsGET[i].first == "_method") - { - overriden = argumentsGET[i].second; - break; - } - } - } - - if (overriden.size() > 0) - { - // A faking has been done within this request - Toolbox::ToUpperCase(overriden); - - LOG(INFO) << "HTTP method faking has been detected for " << overriden; - - if (overriden == "PUT") - { - method = HttpMethod_Put; - return true; - } - else if (overriden == "DELETE") - { - method = HttpMethod_Delete; - return true; - } - else - { - return false; - } - } - - // No PUT/DELETE faking was present - if (!strcmp(request->request_method, "GET")) - { - method = HttpMethod_Get; - } - else if (!strcmp(request->request_method, "POST")) - { - method = HttpMethod_Post; - } - else if (!strcmp(request->request_method, "DELETE")) - { - method = HttpMethod_Delete; - } - else if (!strcmp(request->request_method, "PUT")) - { - method = HttpMethod_Put; - } - else - { - return false; - } - - return true; - } - - - static void ConfigureHttpCompression(HttpOutput& output, - const IHttpHandler::Arguments& headers) - { - // Look if the client wishes HTTP compression - // https://en.wikipedia.org/wiki/HTTP_compression - IHttpHandler::Arguments::const_iterator it = headers.find("accept-encoding"); - if (it != headers.end()) - { - std::vector encodings; - Toolbox::TokenizeString(encodings, it->second, ','); - - for (size_t i = 0; i < encodings.size(); i++) - { - std::string s = Toolbox::StripSpaces(encodings[i]); - - if (s == "deflate") - { - output.SetDeflateAllowed(true); - } - else if (s == "gzip") - { - output.SetGzipAllowed(true); - } - } - } - } - - - static void InternalCallback(HttpOutput& output /* out */, - HttpMethod& method /* out */, - MongooseServer& server, - struct mg_connection *connection, - const struct mg_request_info *request) - { - bool localhost; - -#if ORTHANC_ENABLE_MONGOOSE == 1 - static const long LOCALHOST = (127ll << 24) + 1ll; - localhost = (request->remote_ip == LOCALHOST); -#elif ORTHANC_ENABLE_CIVETWEB == 1 - // The "remote_ip" field of "struct mg_request_info" is tagged as - // deprecated in Civetweb, using "remote_addr" instead. - localhost = (std::string(request->remote_addr) == "127.0.0.1"); -#else -# error -#endif - - // Check remote calls - if (!server.IsRemoteAccessAllowed() && - !localhost) - { - output.SendUnauthorized(server.GetRealm()); - return; - } - - - // Extract the HTTP headers - IHttpHandler::Arguments headers; - for (int i = 0; i < request->num_headers; i++) - { - std::string name = request->http_headers[i].name; - std::string value = request->http_headers[i].value; - - std::transform(name.begin(), name.end(), name.begin(), ::tolower); - headers.insert(std::make_pair(name, value)); - VLOG(1) << "HTTP header: [" << name << "]: [" << value << "]"; - } - - if (server.IsHttpCompressionEnabled()) - { - ConfigureHttpCompression(output, headers); - } - - - // Extract the GET arguments - IHttpHandler::GetArguments argumentsGET; - if (!strcmp(request->request_method, "GET")) - { - HttpToolbox::ParseGetArguments(argumentsGET, request->query_string); - } - - - // Compute the HTTP method, taking method faking into consideration - method = HttpMethod_Get; - if (!ExtractMethod(method, request, headers, argumentsGET)) - { - output.SendStatus(HttpStatus_400_BadRequest); - return; - } - - - // Authenticate this connection - if (server.IsAuthenticationEnabled() && - !IsAccessGranted(server, headers)) - { - output.SendUnauthorized(server.GetRealm()); - return; - } - - -#if ORTHANC_ENABLE_MONGOOSE == 1 - // Apply the filter, if it is installed - char remoteIp[24]; - sprintf(remoteIp, "%d.%d.%d.%d", - reinterpret_cast(&request->remote_ip) [3], - reinterpret_cast(&request->remote_ip) [2], - reinterpret_cast(&request->remote_ip) [1], - reinterpret_cast(&request->remote_ip) [0]); - - const char* requestUri = request->uri; - -#elif ORTHANC_ENABLE_CIVETWEB == 1 - const char* remoteIp = request->remote_addr; - const char* requestUri = request->local_uri; -#else -# error -#endif - - if (requestUri == NULL) - { - requestUri = ""; - } - - std::string username = GetAuthenticatedUsername(headers); - - IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter(); - if (filter != NULL) - { - if (!filter->IsAllowed(method, requestUri, remoteIp, - username.c_str(), headers, argumentsGET)) - { - //output.SendUnauthorized(server.GetRealm()); - output.SendStatus(HttpStatus_403_Forbidden); - return; - } - } - - - // Extract the body of the request for PUT and POST - - // TODO Avoid unneccessary memcopy of the body - - std::string body; - if (method == HttpMethod_Post || - method == HttpMethod_Put) - { - PostDataStatus status; - - IHttpHandler::Arguments::const_iterator ct = headers.find("content-type"); - if (ct == headers.end()) - { - // No content-type specified. Assume no multi-part content occurs at this point. - status = ReadBody(body, connection, headers); - } - else - { - std::string contentType = ct->second; - if (contentType.size() >= multipartLength && - !memcmp(contentType.c_str(), multipart, multipartLength)) - { - status = ParseMultipartPost(body, connection, headers, contentType, server.GetChunkStore()); - } - else - { - status = ReadBody(body, connection, headers); - } - } - - switch (status) - { - case PostDataStatus_NoLength: - output.SendStatus(HttpStatus_411_LengthRequired); - return; - - case PostDataStatus_Failure: - output.SendStatus(HttpStatus_400_BadRequest); - return; - - case PostDataStatus_Pending: - output.AnswerEmpty(); - return; - - default: - break; - } - } - - - // Decompose the URI into its components - UriComponents uri; - try - { - Toolbox::SplitUriComponents(uri, requestUri); - } - catch (OrthancException&) - { - output.SendStatus(HttpStatus_400_BadRequest); - return; - } - - - LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri); - - bool found = false; - - if (server.HasHandler()) - { - found = server.GetHandler().Handle(output, RequestOrigin_RestApi, remoteIp, username.c_str(), - method, uri, headers, argumentsGET, body.c_str(), body.size()); - } - - if (!found) - { - throw OrthancException(ErrorCode_UnknownResource); - } - } - - - static void ProtectedCallback(struct mg_connection *connection, - const struct mg_request_info *request) - { - try - { -#if ORTHANC_ENABLE_MONGOOSE == 1 - void *that = request->user_data; - const char* requestUri = request->uri; -#elif ORTHANC_ENABLE_CIVETWEB == 1 - // https://github.com/civetweb/civetweb/issues/409 - void *that = mg_get_user_data(mg_get_context(connection)); - const char* requestUri = request->local_uri; -#else -# error -#endif - - if (requestUri == NULL) - { - requestUri = ""; - } - - MongooseServer* server = reinterpret_cast(that); - - if (server == NULL) - { - MongooseOutputStream stream(connection); - HttpOutput output(stream, false /* assume no keep-alive */); - output.SendStatus(HttpStatus_500_InternalServerError); - return; - } - - MongooseOutputStream stream(connection); - HttpOutput output(stream, server->IsKeepAliveEnabled()); - HttpMethod method = HttpMethod_Get; - - try - { - try - { - InternalCallback(output, method, *server, connection, request); - } - catch (OrthancException&) - { - throw; // Pass the exception to the main handler below - } - // Now convert native exceptions as OrthancException - catch (boost::bad_lexical_cast&) - { - throw OrthancException(ErrorCode_BadParameterType, - "Syntax error in some user-supplied data"); - } - catch (std::runtime_error&) - { - // Presumably an error while parsing the JSON body - throw OrthancException(ErrorCode_BadRequest); - } - catch (std::bad_alloc&) - { - throw OrthancException(ErrorCode_NotEnoughMemory, - "The server hosting Orthanc is running out of memory"); - } - catch (...) - { - throw OrthancException(ErrorCode_InternalError, - "An unhandled exception was generated inside the HTTP server"); - } - } - catch (OrthancException& e) - { - assert(server != NULL); - - // Using this candidate handler results in an exception - try - { - if (server->GetExceptionFormatter() == NULL) - { - LOG(ERROR) << "Exception in the HTTP handler: " << e.What(); - output.SendStatus(e.GetHttpStatus()); - } - else - { - server->GetExceptionFormatter()->Format(output, e, method, requestUri); - } - } - catch (OrthancException&) - { - // An exception here reflects the fact that the status code - // was already set by the HTTP handler. - } - } - } - catch (...) - { - // We should never arrive at this point, where it is even impossible to send an answer - LOG(ERROR) << "Catastrophic error inside the HTTP server, giving up"; - } - } - - -#if MONGOOSE_USE_CALLBACKS == 0 - static void* Callback(enum mg_event event, - struct mg_connection *connection, - const struct mg_request_info *request) - { - if (event == MG_NEW_REQUEST) - { - ProtectedCallback(connection, request); - - // Mark as processed - return (void*) ""; - } - else - { - return NULL; - } - } - -#elif MONGOOSE_USE_CALLBACKS == 1 - static int Callback(struct mg_connection *connection) - { - const struct mg_request_info *request = mg_get_request_info(connection); - - ProtectedCallback(connection, request); - - return 1; // Do not let Mongoose handle the request by itself - } - -#else -# error Please set MONGOOSE_USE_CALLBACKS -#endif - - - - - - bool MongooseServer::IsRunning() const - { - return (pimpl_->context_ != NULL); - } - - - MongooseServer::MongooseServer() : pimpl_(new PImpl) - { - pimpl_->context_ = NULL; - handler_ = NULL; - remoteAllowed_ = false; - authentication_ = false; - ssl_ = false; - port_ = 8000; - filter_ = NULL; - keepAlive_ = false; - httpCompression_ = true; - exceptionFormatter_ = NULL; - realm_ = ORTHANC_REALM; - threadsCount_ = 50; // Default value in mongoose - -#if ORTHANC_ENABLE_SSL == 1 - // Check for the Heartbleed exploit - // https://en.wikipedia.org/wiki/OpenSSL#Heartbleed_bug - if (OPENSSL_VERSION_NUMBER < 0x1000107fL /* openssl-1.0.1g */ && - OPENSSL_VERSION_NUMBER >= 0x1000100fL /* openssl-1.0.1 */) - { - LOG(WARNING) << "This version of OpenSSL is vulnerable to the Heartbleed exploit"; - } -#endif - } - - - MongooseServer::~MongooseServer() - { - Stop(); - } - - - void MongooseServer::SetPortNumber(uint16_t port) - { - Stop(); - port_ = port; - } - - void MongooseServer::Start() - { -#if ORTHANC_ENABLE_MONGOOSE == 1 - LOG(INFO) << "Starting embedded Web server using Mongoose"; -#elif ORTHANC_ENABLE_CIVETWEB == 1 - LOG(INFO) << "Starting embedded Web server using Civetweb"; -#else -# error -#endif - - if (!IsRunning()) - { - std::string port = boost::lexical_cast(port_); - std::string numThreads = boost::lexical_cast(threadsCount_); - - if (ssl_) - { - port += "s"; - } - - const char *options[] = { - // Set the TCP port for the HTTP server - "listening_ports", port.c_str(), - - // Optimization reported by Chris Hafey - // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ - "enable_keep_alive", (keepAlive_ ? "yes" : "no"), - -#if ORTHANC_ENABLE_CIVETWEB == 1 - // https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#enable_keep_alive-no - "keep_alive_timeout_ms", (keepAlive_ ? "500" : "0"), -#endif - - // Set the number of threads - "num_threads", numThreads.c_str(), - - // Set the SSL certificate, if any. This must be the last option. - ssl_ ? "ssl_certificate" : NULL, - certificate_.c_str(), - NULL - }; - -#if MONGOOSE_USE_CALLBACKS == 0 - pimpl_->context_ = mg_start(&Callback, this, options); - -#elif MONGOOSE_USE_CALLBACKS == 1 - struct mg_callbacks callbacks; - memset(&callbacks, 0, sizeof(callbacks)); - callbacks.begin_request = Callback; - pimpl_->context_ = mg_start(&callbacks, this, options); - -#else -# error Please set MONGOOSE_USE_CALLBACKS -#endif - - if (!pimpl_->context_) - { - throw OrthancException(ErrorCode_HttpPortInUse); - } - - LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber() - << " (HTTPS encryption is " - << (IsSslEnabled() ? "enabled" : "disabled") - << ", remote access is " - << (IsRemoteAccessAllowed() ? "" : "not ") - << "allowed)"; - } - } - - void MongooseServer::Stop() - { - if (IsRunning()) - { - mg_stop(pimpl_->context_); - pimpl_->context_ = NULL; - } - } - - - void MongooseServer::ClearUsers() - { - Stop(); - registeredUsers_.clear(); - } - - - void MongooseServer::RegisterUser(const char* username, - const char* password) - { - Stop(); - - std::string tag = std::string(username) + ":" + std::string(password); - std::string encoded; - Toolbox::EncodeBase64(encoded, tag); - registeredUsers_.insert(encoded); - } - - void MongooseServer::SetSslEnabled(bool enabled) - { - Stop(); - -#if ORTHANC_ENABLE_SSL == 0 - if (enabled) - { - throw OrthancException(ErrorCode_SslDisabled); - } - else - { - ssl_ = false; - } -#else - ssl_ = enabled; -#endif - } - - - void MongooseServer::SetKeepAliveEnabled(bool enabled) - { - Stop(); - keepAlive_ = enabled; - LOG(INFO) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled"); - } - - - void MongooseServer::SetAuthenticationEnabled(bool enabled) - { - Stop(); - authentication_ = enabled; - } - - void MongooseServer::SetSslCertificate(const char* path) - { - Stop(); - certificate_ = path; - } - - void MongooseServer::SetRemoteAccessAllowed(bool allowed) - { - Stop(); - remoteAllowed_ = allowed; - } - - void MongooseServer::SetHttpCompressionEnabled(bool enabled) - { - Stop(); - httpCompression_ = enabled; - LOG(WARNING) << "HTTP compression is " << (enabled ? "enabled" : "disabled"); - } - - void MongooseServer::SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter) - { - Stop(); - filter_ = &filter; - } - - - void MongooseServer::SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter) - { - Stop(); - exceptionFormatter_ = &formatter; - } - - - bool MongooseServer::IsValidBasicHttpAuthentication(const std::string& basic) const - { - return registeredUsers_.find(basic) != registeredUsers_.end(); - } - - - void MongooseServer::Register(IHttpHandler& handler) - { - Stop(); - handler_ = &handler; - } - - - IHttpHandler& MongooseServer::GetHandler() const - { - if (handler_ == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - - return *handler_; - } - - - void MongooseServer::SetThreadsCount(unsigned int threads) - { - if (threads <= 0) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - Stop(); - threadsCount_ = threads; - } -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/HttpServer/MongooseServer.h --- a/Core/HttpServer/MongooseServer.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,210 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 - -#if !defined(ORTHANC_ENABLE_MONGOOSE) -# error Macro ORTHANC_ENABLE_MONGOOSE must be defined to include this file -#endif - -#if !defined(ORTHANC_ENABLE_CIVETWEB) -# error Macro ORTHANC_ENABLE_CIVETWEB must be defined to include this file -#endif - -#if (ORTHANC_ENABLE_MONGOOSE == 0 && \ - ORTHANC_ENABLE_CIVETWEB == 0) -# error Either ORTHANC_ENABLE_MONGOOSE or ORTHANC_ENABLE_CIVETWEB must be set to 1 -#endif - - -#include "IIncomingHttpRequestFilter.h" - -#include -#include -#include -#include -#include - -namespace Orthanc -{ - class ChunkStore; - class OrthancException; - - class IHttpExceptionFormatter : public boost::noncopyable - { - public: - virtual ~IHttpExceptionFormatter() - { - } - - virtual void Format(HttpOutput& output, - const OrthancException& exception, - HttpMethod method, - const char* uri) = 0; - }; - - - class MongooseServer - { - private: - // http://stackoverflow.com/questions/311166/stdauto-ptr-or-boostshared-ptr-for-pimpl-idiom - struct PImpl; - boost::shared_ptr pimpl_; - - IHttpHandler *handler_; - - typedef std::set RegisteredUsers; - RegisteredUsers registeredUsers_; - - bool remoteAllowed_; - bool authentication_; - bool ssl_; - std::string certificate_; - uint16_t port_; - IIncomingHttpRequestFilter* filter_; - bool keepAlive_; - bool httpCompression_; - IHttpExceptionFormatter* exceptionFormatter_; - std::string realm_; - unsigned int threadsCount_; - - bool IsRunning() const; - - public: - MongooseServer(); - - ~MongooseServer(); - - void SetPortNumber(uint16_t port); - - uint16_t GetPortNumber() const - { - return port_; - } - - void Start(); - - void Stop(); - - void ClearUsers(); - - void RegisterUser(const char* username, - const char* password); - - bool IsAuthenticationEnabled() const - { - return authentication_; - } - - void SetAuthenticationEnabled(bool enabled); - - bool IsSslEnabled() const - { - return ssl_; - } - - void SetSslEnabled(bool enabled); - - bool IsKeepAliveEnabled() const - { - return keepAlive_; - } - - void SetKeepAliveEnabled(bool enabled); - - const std::string& GetSslCertificate() const - { - return certificate_; - } - - void SetSslCertificate(const char* path); - - bool IsRemoteAccessAllowed() const - { - return remoteAllowed_; - } - - void SetRemoteAccessAllowed(bool allowed); - - bool IsHttpCompressionEnabled() const - { - return httpCompression_;; - } - - void SetHttpCompressionEnabled(bool enabled); - - IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const - { - return filter_; - } - - void SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter); - - ChunkStore& GetChunkStore(); - - bool IsValidBasicHttpAuthentication(const std::string& basic) const; - - void Register(IHttpHandler& handler); - - bool HasHandler() const - { - return handler_ != NULL; - } - - IHttpHandler& GetHandler() const; - - void SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter); - - IHttpExceptionFormatter* GetExceptionFormatter() - { - return exceptionFormatter_; - } - - const std::string& GetRealm() const - { - return realm_; - } - - void SetRealm(const std::string& realm) - { - realm_ = realm; - } - - void SetThreadsCount(unsigned int threads); - - unsigned int GetThreadsCount() const - { - return threadsCount_; - } - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/HttpServer/StringHttpOutput.h --- a/Core/HttpServer/StringHttpOutput.h Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/HttpServer/StringHttpOutput.h Thu Jan 24 10:55:19 2019 +0100 @@ -54,6 +54,10 @@ virtual void Send(bool isHeader, const void* buffer, size_t length); + virtual void DisableKeepAlive() + { + } + void GetOutput(std::string& output); }; } diff -r 4cfed5c2eacd -r fc9a4a2dad63 Core/SerializationToolbox.cpp --- a/Core/SerializationToolbox.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Core/SerializationToolbox.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -53,7 +53,7 @@ tag = FromDcmtkBridge::ParseTag(name); return true; } - catch (OrthancException& e) + catch (OrthancException&) { return false; } diff -r 4cfed5c2eacd -r fc9a4a2dad63 LinuxCompilation.txt --- a/LinuxCompilation.txt Thu Jan 24 10:54:47 2019 +0100 +++ b/LinuxCompilation.txt Thu Jan 24 10:55:19 2019 +0100 @@ -85,7 +85,7 @@ libboost-all-dev libwrap0-dev libjsoncpp-dev libpugixml-dev # cmake -DALLOW_DOWNLOADS=ON \ - -DUSE_SYSTEM_MONGOOSE=OFF \ + -DUSE_SYSTEM_CIVETWEB=OFF \ -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ -DDCMTK_LIBRARIES=dcmjpls \ -DCMAKE_BUILD_TYPE=Release \ @@ -106,7 +106,7 @@ # cmake "-DDCMTK_LIBRARIES=boost_locale;CharLS;dcmjpls;wrap;oflog" \ -DALLOW_DOWNLOADS=ON \ - -DUSE_SYSTEM_MONGOOSE=OFF \ + -DUSE_SYSTEM_CIVETWEB=OFF \ -DUSE_SYSTEM_JSONCPP=OFF \ -DUSE_SYSTEM_PUGIXML=OFF \ -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ @@ -126,7 +126,7 @@ # cmake -DALLOW_DOWNLOADS=ON \ -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \ - -DUSE_SYSTEM_MONGOOSE=OFF \ + -DUSE_SYSTEM_CIVETWEB=OFF \ -DDCMTK_LIBRARIES=dcmjpls \ -DCMAKE_BUILD_TYPE=Release \ ~/Orthanc @@ -146,6 +146,7 @@ # sudo yum install gflags-devel # cmake "-DDCMTK_LIBRARIES=CharLS" \ + -DENABLE_CIVETWEB=OFF \ -DSYSTEM_MONGOOSE_USE_CALLBACKS=OFF \ -DCMAKE_BUILD_TYPE=Release \ ~/Orthanc @@ -162,7 +163,7 @@ e2fsprogs-libuuid boost-libs sqlite3 python libiconv # cmake -DALLOW_DOWNLOADS=ON \ - -DUSE_SYSTEM_MONGOOSE=OFF \ + -DUSE_SYSTEM_CIVETWEB=OFF \ -DDCMTK_LIBRARIES="dcmdsig;charls;dcmjpls" \ -DCMAKE_BUILD_TYPE=Release \ ~/Orthanc @@ -178,7 +179,7 @@ # cmake -DALLOW_DOWNLOADS=ON \ -DUSE_SYSTEM_JSONCPP=OFF \ - -DUSE_SYSTEM_MONGOOSE=OFF \ + -DUSE_SYSTEM_CIVETWEB=OFF \ -DUSE_SYSTEM_PUGIXML=OFF \ -DUSE_SYSTEM_SQLITE=OFF \ -DUSE_SYSTEM_BOOST=OFF \ diff -r 4cfed5c2eacd -r fc9a4a2dad63 NEWS --- a/NEWS Thu Jan 24 10:54:47 2019 +0100 +++ b/NEWS Thu Jan 24 10:55:19 2019 +0100 @@ -1,12 +1,44 @@ Pending changes in the mainline =============================== +* Fix compatibility with DICOMweb plugin (allow multipart answers over HTTP Keep-Alive) +* Fix issue #128 (Asynchronous C-MOVE: invalid number of remaining sub-operations) +* Don't return tags whose group is below 0x0008 in C-FIND SCP + + +Version 1.5.2 (2019-01-18) +========================== + +General +------- + +* CivetWeb is now the default embedded HTTP server (instead of Mongoose) +* New configuration option: "TcpNoDelay" to disable Nagle's algorithm in HTTP server + +REST API +-------- + +* API version has been upgraded to 1.3 +* More consistent handling of the "Last" field returned by the "/changes" URI + +Plugins +------- + +* New primitives to speed up databases (custom index plugins) + Maintenance ----------- +* Don't consider tags whose group is below 0x0008 in C-FIND SCP +* Compatibility with DCMTK 3.6.4 +* Fix issue #21 (DICOM files missing after uploading with Firefox) +* Fix issue #32 (HTTP keep-alive is now enabled by default) +* Fix issue #58 (Patient recycling order should be defined by their received last instance) * Fix issue #118 (Wording in Configuration.json regarding SynchronousCMove) +* Fix issue #124 (GET /studies/ID/media fails for certain dicom file) +* Fix issue #125 (Mongoose: /instances/{id} returns 500 on invalid HTTP Method) * Fixed Orthanc Explorer on IE and Firefox: Explorer always show "too many results" - and it's therefore impossible to browse the content. + and it's therefore impossible to browse the content * Upgraded dependencies for static and Windows builds: - civetweb 1.11 @@ -49,7 +81,7 @@ REST API -------- -* API Version has been upgraded to 1.2 +* API version has been upgraded to 1.2 * Asynchronous generation of ZIP archives and DICOM medias * New URI: "/studies/.../merge" to merge a study * New URI: "/studies/.../split" to split a study diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancExplorer/explorer.html --- a/OrthancExplorer/explorer.html Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancExplorer/explorer.html Thu Jan 24 10:55:19 2019 +0100 @@ -172,31 +172,34 @@

+
+

Warning:

Orthanc issue #21: On Firefox, especially on + Linux & OSX systems, files might be missing if using + drag-and-drop. Please use the "Select files to upload" button + instead, or use the command-line "ImportDicomFiles.py" script. +
  • Drag and drop DICOM files here
-
-

Warning:

Orthanc issue #21: On Firefox, especially on Linux & OSX systems, files might be missing when - using drag-and-drop. Use the "Select files to upload" button instead ! -
@@ -636,4 +639,4 @@ - \ No newline at end of file + diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancExplorer/file-upload.js --- a/OrthancExplorer/file-upload.js Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancExplorer/file-upload.js Thu Jan 24 10:55:19 2019 +0100 @@ -61,16 +61,22 @@ appendFilesToUploadList(e.target.files); }) -$('#upload').live('pageshow', function() { + +function ClearUploadProgress() +{ + $('#progress .label').text(''); + $('#progress .bar').css('width', '0%').css('background-color', '#333'); +} + +$('#upload').live('pagebeforeshow', function() { if (navigator.userAgent.toLowerCase().indexOf('firefox') == -1) { $("#issue-21-warning").css('display', 'none'); } - // alert('WARNING - This page is currently affected by Orthanc issue #21: ' + - // '"DICOM files might be missing after uploading with Mozilla Firefox." ' + - // 'Do not use this upload feature for clinical uses, or carefully ' + - // 'check that all instances have been properly received by Orthanc. ' + - // 'Please use the command-line "ImportDicomFiles.py" script to circumvent this issue.'); + ClearUploadProgress(); +}); + +$('#upload').live('pageshow', function() { $('#fileupload').fileupload('enable'); }); @@ -85,8 +91,7 @@ $('.pending-file').remove(); $('#upload-list').listview('refresh'); - $('#progress .bar').css('width', '0%'); - $('#progress .label').text(''); + ClearUploadProgress(); currentUpload = 1; totalUploads = pu.length + 1; diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/DatabaseLookup.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/DatabaseLookup.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,420 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../../../Core/OrthancException.h" +#include "../../Search/DicomTagConstraint.h" +#include "../../ServerToolbox.h" +#include "SetOfResources.h" + +namespace Orthanc +{ + namespace Compatibility + { + namespace + { + // Anonymous namespace to avoid clashes between compiler modules + class MainTagsConstraints : boost::noncopyable + { + private: + std::vector constraints_; + + public: + ~MainTagsConstraints() + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + delete constraints_[i]; + } + } + + void Reserve(size_t n) + { + constraints_.reserve(n); + } + + size_t GetSize() const + { + return constraints_.size(); + } + + DicomTagConstraint& GetConstraint(size_t i) const + { + if (i >= constraints_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(constraints_[i] != NULL); + return *constraints_[i]; + } + } + + void Add(const DatabaseConstraint& constraint) + { + constraints_.push_back(new DicomTagConstraint(constraint)); + } + }; + } + + + static void ApplyIdentifierConstraint(SetOfResources& candidates, + ILookupResources& compatibility, + const DatabaseConstraint& constraint, + ResourceType level) + { + std::list matches; + + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + compatibility.LookupIdentifier(matches, level, constraint.GetTag(), + IdentifierConstraintType_Equal, constraint.GetSingleValue()); + break; + + case ConstraintType_SmallerOrEqual: + compatibility.LookupIdentifier(matches, level, constraint.GetTag(), + IdentifierConstraintType_SmallerOrEqual, constraint.GetSingleValue()); + break; + + case ConstraintType_GreaterOrEqual: + compatibility.LookupIdentifier(matches, level, constraint.GetTag(), + IdentifierConstraintType_GreaterOrEqual, constraint.GetSingleValue()); + + break; + + case ConstraintType_Wildcard: + compatibility.LookupIdentifier(matches, level, constraint.GetTag(), + IdentifierConstraintType_Wildcard, constraint.GetSingleValue()); + + break; + + case ConstraintType_List: + for (size_t i = 0; i < constraint.GetValuesCount(); i++) + { + std::list tmp; + compatibility.LookupIdentifier(tmp, level, constraint.GetTag(), + IdentifierConstraintType_Wildcard, constraint.GetValue(i)); + matches.splice(matches.end(), tmp); + } + + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + candidates.Intersect(matches); + } + + + static void ApplyIdentifierRange(SetOfResources& candidates, + ILookupResources& compatibility, + const DatabaseConstraint& smaller, + const DatabaseConstraint& greater, + ResourceType level) + { + assert(smaller.GetConstraintType() == ConstraintType_SmallerOrEqual && + greater.GetConstraintType() == ConstraintType_GreaterOrEqual && + smaller.GetTag() == greater.GetTag() && + ServerToolbox::IsIdentifier(smaller.GetTag(), level)); + + std::list matches; + compatibility.LookupIdentifierRange(matches, level, smaller.GetTag(), + greater.GetSingleValue(), smaller.GetSingleValue()); + candidates.Intersect(matches); + } + + + static void ApplyLevel(SetOfResources& candidates, + IDatabaseWrapper& database, + ILookupResources& compatibility, + const std::vector& lookup, + ResourceType level) + { + typedef std::set SetOfConstraints; + typedef std::map Identifiers; + + // (1) Select which constraints apply to this level, and split + // them between "identifier tags" constraints and "main DICOM + // tags" constraints + + Identifiers identifiers; + SetOfConstraints mainTags; + + for (size_t i = 0; i < lookup.size(); i++) + { + if (lookup[i].GetLevel() == level) + { + if (lookup[i].IsIdentifier()) + { + identifiers[lookup[i].GetTag()].insert(&lookup[i]); + } + else + { + mainTags.insert(&lookup[i]); + } + } + } + + + // (2) Apply the constraints over the identifiers + + for (Identifiers::const_iterator it = identifiers.begin(); + it != identifiers.end(); ++it) + { + // Check whether some range constraint over identifiers is + // present at this level + const DatabaseConstraint* smaller = NULL; + const DatabaseConstraint* greater = NULL; + + for (SetOfConstraints::const_iterator it2 = it->second.begin(); + it2 != it->second.end(); ++it2) + { + assert(*it2 != NULL); + + if ((*it2)->GetConstraintType() == ConstraintType_SmallerOrEqual) + { + smaller = *it2; + } + + if ((*it2)->GetConstraintType() == ConstraintType_GreaterOrEqual) + { + greater = *it2; + } + } + + if (smaller != NULL && + greater != NULL) + { + // There is a range constraint: Apply it, as it is more efficient + ApplyIdentifierRange(candidates, compatibility, *smaller, *greater, level); + } + else + { + smaller = NULL; + greater = NULL; + } + + for (SetOfConstraints::const_iterator it2 = it->second.begin(); + it2 != it->second.end(); ++it2) + { + // Check to avoid applying twice the range constraint + if (*it2 != smaller && + *it2 != greater) + { + ApplyIdentifierConstraint(candidates, compatibility, **it2, level); + } + } + } + + + // (3) Apply the constraints over the main DICOM tags (no index + // here, so this is less efficient than filtering over the + // identifiers) + if (!mainTags.empty()) + { + MainTagsConstraints c; + c.Reserve(mainTags.size()); + + for (SetOfConstraints::const_iterator it = mainTags.begin(); + it != mainTags.end(); ++it) + { + assert(*it != NULL); + c.Add(**it); + } + + std::list source; + candidates.Flatten(compatibility, source); + candidates.Clear(); + + std::list filtered; + for (std::list::const_iterator candidate = source.begin(); + candidate != source.end(); ++candidate) + { + DicomMap tags; + database.GetMainDicomTags(tags, *candidate); + + bool match = true; + + for (size_t i = 0; i < c.GetSize(); i++) + { + if (!c.GetConstraint(i).IsMatch(tags)) + { + match = false; + break; + } + } + + if (match) + { + filtered.push_back(*candidate); + } + } + + candidates.Intersect(filtered); + } + } + + + static std::string GetOneInstance(IDatabaseWrapper& compatibility, + int64_t resource, + ResourceType level) + { + for (int i = level; i < ResourceType_Instance; i++) + { + assert(compatibility.GetResourceType(resource) == static_cast(i)); + + std::list children; + compatibility.GetChildrenInternalId(children, resource); + + if (children.empty()) + { + throw OrthancException(ErrorCode_Database); + } + + resource = children.front(); + } + + return compatibility.GetPublicId(resource); + } + + + void DatabaseLookup::ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) + { + // This is a re-implementation of + // "../../../Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp" + + assert(ResourceType_Patient < ResourceType_Study && + ResourceType_Study < ResourceType_Series && + ResourceType_Series < ResourceType_Instance); + + ResourceType upperLevel = queryLevel; + ResourceType lowerLevel = queryLevel; + + for (size_t i = 0; i < lookup.size(); i++) + { + ResourceType level = lookup[i].GetLevel(); + + if (level < upperLevel) + { + upperLevel = level; + } + + if (level > lowerLevel) + { + lowerLevel = level; + } + } + + assert(upperLevel <= queryLevel && + queryLevel <= lowerLevel); + + SetOfResources candidates(database_, upperLevel); + + for (int level = upperLevel; level <= lowerLevel; level++) + { + ApplyLevel(candidates, database_, compatibility_, lookup, static_cast(level)); + + if (level != lowerLevel) + { + candidates.GoDown(); + } + } + + std::list resources; + candidates.Flatten(compatibility_, resources); + + // Climb up, up to queryLevel + + for (int level = lowerLevel; level > queryLevel; level--) + { + std::list parents; + for (std::list::const_iterator + it = resources.begin(); it != resources.end(); ++it) + { + int64_t parent; + if (database_.LookupParent(parent, *it)) + { + parents.push_back(parent); + } + } + + resources.swap(parents); + } + + // Apply the limit, if given + + if (limit != 0 && + resources.size() > limit) + { + resources.resize(limit); + } + + // Get the public ID of all the selected resources + + size_t pos = 0; + + for (std::list::const_iterator + it = resources.begin(); it != resources.end(); ++it, pos++) + { + assert(database_.GetResourceType(*it) == queryLevel); + + const std::string resource = database_.GetPublicId(*it); + resourcesId.push_back(resource); + + if (instancesId != NULL) + { + if (queryLevel == ResourceType_Instance) + { + // The resource is itself the instance + instancesId->push_back(resource); + } + else + { + // Collect one child instance for each of the selected resources + instancesId->push_back(GetOneInstance(database_, *it, queryLevel)); + } + } + } + } + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/DatabaseLookup.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/DatabaseLookup.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,64 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../IDatabaseWrapper.h" +#include "ILookupResources.h" + +namespace Orthanc +{ + namespace Compatibility + { + class DatabaseLookup : public boost::noncopyable + { + private: + IDatabaseWrapper& database_; + ILookupResources& compatibility_; + + public: + DatabaseLookup(IDatabaseWrapper& database, + ILookupResources& compatibility) : + database_(database), + compatibility_(compatibility) + { + } + + void ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit); + }; + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/ICreateInstance.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/ICreateInstance.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,157 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "ICreateInstance.h" + +#include "../../../Core/OrthancException.h" + +namespace Orthanc +{ + namespace Compatibility + { + bool ICreateInstance::Apply(ICreateInstance& database, + IDatabaseWrapper::CreateInstanceResult& result, + int64_t& instanceId, + const std::string& hashPatient, + const std::string& hashStudy, + const std::string& hashSeries, + const std::string& hashInstance) + { + { + ResourceType type; + int64_t tmp; + + if (database.LookupResource(tmp, type, hashInstance)) + { + // The instance already exists + assert(type == ResourceType_Instance); + instanceId = tmp; + return false; + } + } + + instanceId = database.CreateResource(hashInstance, ResourceType_Instance); + + result.isNewPatient_ = false; + result.isNewStudy_ = false; + result.isNewSeries_ = false; + result.patientId_ = -1; + result.studyId_ = -1; + result.seriesId_ = -1; + + // Detect up to which level the patient/study/series/instance + // hierarchy must be created + + { + ResourceType dummy; + + if (database.LookupResource(result.seriesId_, dummy, hashSeries)) + { + assert(dummy == ResourceType_Series); + // The patient, the study and the series already exist + + bool ok = (database.LookupResource(result.patientId_, dummy, hashPatient) && + database.LookupResource(result.studyId_, dummy, hashStudy)); + assert(ok); + } + else if (database.LookupResource(result.studyId_, dummy, hashStudy)) + { + assert(dummy == ResourceType_Study); + + // New series: The patient and the study already exist + result.isNewSeries_ = true; + + bool ok = database.LookupResource(result.patientId_, dummy, hashPatient); + assert(ok); + } + else if (database.LookupResource(result.patientId_, dummy, hashPatient)) + { + assert(dummy == ResourceType_Patient); + + // New study and series: The patient already exist + result.isNewStudy_ = true; + result.isNewSeries_ = true; + } + else + { + // New patient, study and series: Nothing exists + result.isNewPatient_ = true; + result.isNewStudy_ = true; + result.isNewSeries_ = true; + } + } + + // Create the series if needed + if (result.isNewSeries_) + { + result.seriesId_ = database.CreateResource(hashSeries, ResourceType_Series); + } + + // Create the study if needed + if (result.isNewStudy_) + { + result.studyId_ = database.CreateResource(hashStudy, ResourceType_Study); + } + + // Create the patient if needed + if (result.isNewPatient_) + { + result.patientId_ = database.CreateResource(hashPatient, ResourceType_Patient); + } + + // Create the parent-to-child links + database.AttachChild(result.seriesId_, instanceId); + + if (result.isNewSeries_) + { + database.AttachChild(result.studyId_, result.seriesId_); + } + + if (result.isNewStudy_) + { + database.AttachChild(result.patientId_, result.studyId_); + } + + database.TagMostRecentPatient(result.patientId_); + + // Sanity checks + assert(result.patientId_ != -1); + assert(result.studyId_ != -1); + assert(result.seriesId_ != -1); + assert(instanceId != -1); + + return true; + } + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/ICreateInstance.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/ICreateInstance.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,66 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../IDatabaseWrapper.h" + +namespace Orthanc +{ + namespace Compatibility + { + class ICreateInstance : public boost::noncopyable + { + public: + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) = 0; + + virtual int64_t CreateResource(const std::string& publicId, + ResourceType type) = 0; + + virtual void AttachChild(int64_t parent, + int64_t child) = 0; + + virtual void TagMostRecentPatient(int64_t patientId) = 0; + + static bool Apply(ICreateInstance& database, + IDatabaseWrapper::CreateInstanceResult& result, + int64_t& instanceId, + const std::string& patient, + const std::string& study, + const std::string& series, + const std::string& instance); + }; + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/IGetChildrenMetadata.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/IGetChildrenMetadata.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,66 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "IGetChildrenMetadata.h" + +namespace Orthanc +{ + namespace Compatibility + { + void IGetChildrenMetadata::Apply(IGetChildrenMetadata& database, + std::list& target, + int64_t resourceId, + MetadataType metadata) + { + // This function comes from an optimization of + // "ServerIndex::GetSeriesStatus()" in Orthanc <= 1.5.1 + // Loop over the instances of this series + + target.clear(); + + std::list children; + database.GetChildrenInternalId(children, resourceId); + + for (std::list::const_iterator + it = children.begin(); it != children.end(); ++it) + { + std::string value; + if (database.LookupMetadata(value, *it, metadata)) + { + target.push_back(value); + } + } + } + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/IGetChildrenMetadata.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/IGetChildrenMetadata.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,61 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 +#include + +namespace Orthanc +{ + namespace Compatibility + { + class IGetChildrenMetadata : public boost::noncopyable + { + public: + virtual void GetChildrenInternalId(std::list& target, + int64_t id) = 0; + + virtual bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type) = 0; + + static void Apply(IGetChildrenMetadata& database, + std::list& target, + int64_t resourceId, + MetadataType metadata); + }; + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/ILookupResources.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/ILookupResources.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,56 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "ILookupResources.h" + +#include "DatabaseLookup.h" + +namespace Orthanc +{ + namespace Compatibility + { + void ILookupResources::Apply( + IDatabaseWrapper& database, + ILookupResources& compatibility, + std::list& resourcesId, + std::list* instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) + { + Compatibility::DatabaseLookup compat(database, compatibility); + compat.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit); + } + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/ILookupResources.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/ILookupResources.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../IDatabaseWrapper.h" + +namespace Orthanc +{ + namespace Compatibility + { + /** + * This is a compatibility class that contains database primitives + * that were used in Orthanc <= 1.5.1, and that have been removed + * during the optimization of the database engine. + **/ + class ILookupResources : public boost::noncopyable + { + public: + virtual ~ILookupResources() + { + } + + virtual void GetAllInternalIds(std::list& target, + ResourceType resourceType) = 0; + + virtual void LookupIdentifier(std::list& result, + ResourceType level, + const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value) = 0; + + virtual void LookupIdentifierRange(std::list& result, + ResourceType level, + const DicomTag& tag, + const std::string& start, + const std::string& end) = 0; + + static void Apply(IDatabaseWrapper& database, + ILookupResources& compatibility, + std::list& resourcesId, + std::list* instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit); + }; + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/ISetResourcesContent.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/ISetResourcesContent.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,68 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../ResourcesContent.h" + +namespace Orthanc +{ + namespace Compatibility + { + class ISetResourcesContent : public boost::noncopyable + { + public: + virtual ~ISetResourcesContent() + { + } + + virtual void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) = 0; + + virtual void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) = 0; + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value) = 0; + + static void Apply(ISetResourcesContent& that, + const ResourcesContent& content) + { + content.Store(that); + } + }; + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/SetOfResources.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/SetOfResources.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,161 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "SetOfResources.h" + +#include "../../../Core/OrthancException.h" + + +namespace Orthanc +{ + namespace Compatibility + { + void SetOfResources::Intersect(const std::list& resources) + { + if (resources_.get() == NULL) + { + resources_.reset(new Resources); + + for (std::list::const_iterator + it = resources.begin(); it != resources.end(); ++it) + { + resources_->insert(*it); + } + } + else + { + std::auto_ptr filtered(new Resources); + + for (std::list::const_iterator + it = resources.begin(); it != resources.end(); ++it) + { + if (resources_->find(*it) != resources_->end()) + { + filtered->insert(*it); + } + } + + resources_ = filtered; + } + } + + + void SetOfResources::GoDown() + { + if (level_ == ResourceType_Instance) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (resources_.get() != NULL) + { + std::auto_ptr children(new Resources); + + for (Resources::const_iterator it = resources_->begin(); + it != resources_->end(); ++it) + { + std::list tmp; + database_.GetChildrenInternalId(tmp, *it); + + for (std::list::const_iterator + child = tmp.begin(); child != tmp.end(); ++child) + { + children->insert(*child); + } + } + + resources_ = children; + } + + switch (level_) + { + case ResourceType_Patient: + level_ = ResourceType_Study; + break; + + case ResourceType_Study: + level_ = ResourceType_Series; + break; + + case ResourceType_Series: + level_ = ResourceType_Instance; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + void SetOfResources::Flatten(std::list& result) + { + result.clear(); + + if (resources_.get() == NULL) + { + // All the resources of this level are part of the filter + database_.GetAllPublicIds(result, level_); + } + else + { + for (Resources::const_iterator it = resources_->begin(); + it != resources_->end(); ++it) + { + result.push_back(database_.GetPublicId(*it)); + } + } + } + + + void SetOfResources::Flatten(ILookupResources& compatibility, + std::list& result) + { + result.clear(); + + if (resources_.get() == NULL) + { + // All the resources of this level are part of the filter + compatibility.GetAllInternalIds(result, level_); + } + else + { + for (Resources::const_iterator it = resources_->begin(); + it != resources_->end(); ++it) + { + result.push_back(*it); + } + } + } + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Compatibility/SetOfResources.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Compatibility/SetOfResources.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,83 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../IDatabaseWrapper.h" +#include "ILookupResources.h" + +#include +#include + +namespace Orthanc +{ + namespace Compatibility + { + class SetOfResources : public boost::noncopyable + { + private: + typedef std::set Resources; + + IDatabaseWrapper& database_; + ResourceType level_; + std::auto_ptr resources_; + + public: + SetOfResources(IDatabaseWrapper& database, + ResourceType level) : + database_(database), + level_(level) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + void Intersect(const std::list& resources); + + void GoDown(); + + void Flatten(ILookupResources& compatibility, + std::list& result); + + void Flatten(std::list& result); + + void Clear() + { + resources_.reset(NULL); + } + }; + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/IDatabaseListener.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/IDatabaseListener.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,57 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../ServerIndexChange.h" + +#include + +namespace Orthanc +{ + class IDatabaseListener : public boost::noncopyable + { + public: + virtual ~IDatabaseListener() + { + } + + virtual void SignalRemainingAncestor(ResourceType parentType, + const std::string& publicId) = 0; + + virtual void SignalFileDeleted(const FileInfo& info) = 0; + + virtual void SignalChange(const ServerIndexChange& change) = 0; + }; +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/IDatabaseWrapper.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/IDatabaseWrapper.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,249 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../../Core/DicomFormat/DicomMap.h" +#include "../../Core/FileStorage/FileInfo.h" +#include "../../Core/FileStorage/IStorageArea.h" +#include "../../Core/SQLite/ITransaction.h" + +#include "../ExportedResource.h" +#include "IDatabaseListener.h" + +#include +#include + +namespace Orthanc +{ + class DatabaseConstraint; + class ResourcesContent; + + + class IDatabaseWrapper : public boost::noncopyable + { + public: + class ITransaction : public boost::noncopyable + { + public: + virtual ~ITransaction() + { + } + + virtual void Begin() = 0; + + virtual void Rollback() = 0; + + virtual void Commit(int64_t fileSizeDelta) = 0; + }; + + + struct CreateInstanceResult + { + bool isNewPatient_; + bool isNewStudy_; + bool isNewSeries_; + int64_t patientId_; + int64_t studyId_; + int64_t seriesId_; + }; + + virtual ~IDatabaseWrapper() + { + } + + virtual void Open() = 0; + + virtual void Close() = 0; + + virtual void AddAttachment(int64_t id, + const FileInfo& attachment) = 0; + + virtual void ClearChanges() = 0; + + virtual void ClearExportedResources() = 0; + + virtual void DeleteAttachment(int64_t id, + FileContentType attachment) = 0; + + virtual void DeleteMetadata(int64_t id, + MetadataType type) = 0; + + virtual void DeleteResource(int64_t id) = 0; + + virtual void FlushToDisk() = 0; + + virtual bool HasFlushToDisk() const = 0; + + virtual void GetAllMetadata(std::map& target, + int64_t id) = 0; + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType) = 0; + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) = 0; + + virtual void GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) = 0; + + virtual void GetChildrenInternalId(std::list& target, + int64_t id) = 0; + + virtual void GetChildrenPublicId(std::list& target, + int64_t id) = 0; + + virtual void GetExportedResources(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) = 0; + + virtual void GetLastChange(std::list& target /*out*/) = 0; + + virtual void GetLastExportedResource(std::list& target /*out*/) = 0; + + virtual void GetMainDicomTags(DicomMap& map, + int64_t id) = 0; + + virtual std::string GetPublicId(int64_t resourceId) = 0; + + virtual uint64_t GetResourceCount(ResourceType resourceType) = 0; + + virtual ResourceType GetResourceType(int64_t resourceId) = 0; + + virtual uint64_t GetTotalCompressedSize() = 0; + + virtual uint64_t GetTotalUncompressedSize() = 0; + + virtual bool IsExistingResource(int64_t internalId) = 0; + + virtual bool IsProtectedPatient(int64_t internalId) = 0; + + virtual void ListAvailableMetadata(std::list& target, + int64_t id) = 0; + + virtual void ListAvailableAttachments(std::list& target, + int64_t id) = 0; + + virtual void LogChange(int64_t internalId, + const ServerIndexChange& change) = 0; + + virtual void LogExportedResource(const ExportedResource& resource) = 0; + + virtual bool LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType) = 0; + + virtual bool LookupGlobalProperty(std::string& target, + GlobalProperty property) = 0; + + virtual bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type) = 0; + + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId) = 0; + + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) = 0; + + virtual bool SelectPatientToRecycle(int64_t& internalId) = 0; + + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) = 0; + + virtual void SetGlobalProperty(GlobalProperty property, + const std::string& value) = 0; + + virtual void ClearMainDicomTags(int64_t id) = 0; + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value) = 0; + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected) = 0; + + virtual ITransaction* StartTransaction() = 0; + + virtual void SetListener(IDatabaseListener& listener) = 0; + + virtual unsigned int GetDatabaseVersion() = 0; + + virtual void Upgrade(unsigned int targetVersion, + IStorageArea& storageArea) = 0; + + + /** + * Primitives introduced in Orthanc 1.5.2 + **/ + + virtual bool IsDiskSizeAbove(uint64_t threshold) = 0; + + virtual void ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, // Can be NULL if not needed + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) = 0; + + // Returns "true" iff. the instance is new and has been inserted + // into the database. If "false" is returned, the content of + // "result" is undefined, but "instanceId" must be properly + // set. This method must also tag the parent patient as the most + // recent in the patient recycling order if it is not protected + // (so as to fix issue #58). + virtual bool CreateInstance(CreateInstanceResult& result, /* out */ + int64_t& instanceId, /* out */ + const std::string& patient, + const std::string& study, + const std::string& series, + const std::string& instance) = 0; + + // It is guaranteed that the resources to be modified have no main + // DICOM tags, and no DICOM identifiers associated with + // them. However, some metadata might be already existing, and + // have to be overwritten. + virtual void SetResourcesContent(const ResourcesContent& content) = 0; + + virtual void GetChildrenMetadata(std::list& target, + int64_t resourceId, + MetadataType metadata) = 0; + + virtual int64_t GetLastChangeIndex() = 0; + }; +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/InstallTrackAttachmentsSize.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/InstallTrackAttachmentsSize.sql Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,22 @@ +CREATE TABLE GlobalIntegers( + key INTEGER PRIMARY KEY, + value INTEGER); + +INSERT INTO GlobalProperties VALUES (6, 1); -- GlobalProperty_GetTotalSizeIsFast + +INSERT INTO GlobalIntegers SELECT 0, IFNULL(SUM(compressedSize), 0) FROM AttachedFiles; +INSERT INTO GlobalIntegers SELECT 1, IFNULL(SUM(uncompressedSize), 0) FROM AttachedFiles; + +CREATE TRIGGER AttachedFileIncrementSize +AFTER INSERT ON AttachedFiles +BEGIN + UPDATE GlobalIntegers SET value = value + new.compressedSize WHERE key = 0; + UPDATE GlobalIntegers SET value = value + new.uncompressedSize WHERE key = 1; +END; + +CREATE TRIGGER AttachedFileDecrementSize +AFTER DELETE ON AttachedFiles +BEGIN + UPDATE GlobalIntegers SET value = value - old.compressedSize WHERE key = 0; + UPDATE GlobalIntegers SET value = value - old.uncompressedSize WHERE key = 1; +END; diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/PrepareDatabase.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/PrepareDatabase.sql Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,126 @@ +CREATE TABLE GlobalProperties( + property INTEGER PRIMARY KEY, + value TEXT + ); + +CREATE TABLE Resources( + internalId INTEGER PRIMARY KEY AUTOINCREMENT, + resourceType INTEGER, + publicId TEXT, + parentId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE + ); + +CREATE TABLE MainDicomTags( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + tagGroup INTEGER, + tagElement INTEGER, + value TEXT, + PRIMARY KEY(id, tagGroup, tagElement) + ); + +-- The following table was added in Orthanc 0.8.5 (database v5) +CREATE TABLE DicomIdentifiers( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + tagGroup INTEGER, + tagElement INTEGER, + value TEXT, + PRIMARY KEY(id, tagGroup, tagElement) + ); + +CREATE TABLE Metadata( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + type INTEGER, + value TEXT, + PRIMARY KEY(id, type) + ); + +CREATE TABLE AttachedFiles( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + fileType INTEGER, + uuid TEXT, + compressedSize INTEGER, + uncompressedSize INTEGER, + compressionType INTEGER, + uncompressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) + compressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) + PRIMARY KEY(id, fileType) + ); + +CREATE TABLE Changes( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + changeType INTEGER, + internalId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + resourceType INTEGER, + date TEXT + ); + +CREATE TABLE ExportedResources( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + resourceType INTEGER, + publicId TEXT, + remoteModality TEXT, + patientId TEXT, + studyInstanceUid TEXT, + seriesInstanceUid TEXT, + sopInstanceUid TEXT, + date TEXT + ); + +CREATE TABLE PatientRecyclingOrder( + seq INTEGER PRIMARY KEY AUTOINCREMENT, + patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE + ); + +CREATE INDEX ChildrenIndex ON Resources(parentId); +CREATE INDEX PublicIndex ON Resources(publicId); +CREATE INDEX ResourceTypeIndex ON Resources(resourceType); +CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId); + +CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); +-- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up +-- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); +-- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); + +-- The 3 following indexes were added in Orthanc 0.8.5 (database v5) +CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); +CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement); +CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY); + +CREATE INDEX ChangesIndex ON Changes(internalId); + +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +BEGIN + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize, + -- These 2 arguments are new in Orthanc 0.7.3 (database v4) + old.uncompressedMD5, old.compressedMD5); +END; + +CREATE TRIGGER ResourceDeleted +AFTER DELETE ON Resources +BEGIN + SELECT SignalResourceDeleted(old.publicId, old.resourceType); -- New in Orthanc 0.8.5 (db v5) + SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) + FROM Resources AS parent WHERE internalId = old.parentId; +END; + +-- Delete a parent resource when its unique child is deleted +CREATE TRIGGER ResourceDeletedParentCleaning +AFTER DELETE ON Resources +FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0 +BEGIN + DELETE FROM Resources WHERE internalId = old.parentId; +END; + +CREATE TRIGGER PatientAdded +AFTER INSERT ON Resources +FOR EACH ROW WHEN new.resourceType = 1 -- "1" corresponds to "ResourceType_Patient" in C++ +BEGIN + INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId); +END; + + +-- Set the version of the database schema +-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration +INSERT INTO GlobalProperties VALUES (1, "6"); diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/ResourcesContent.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/ResourcesContent.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,65 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "ResourcesContent.h" + +#include "Compatibility/ISetResourcesContent.h" + +#include + + +namespace Orthanc +{ + void ResourcesContent::Store(Compatibility::ISetResourcesContent& compatibility) const + { + for (std::list::const_iterator + it = tags_.begin(); it != tags_.end(); ++it) + { + if (it->isIdentifier_) + { + compatibility.SetIdentifierTag(it->resourceId_, it->tag_, it->value_); + } + else + { + compatibility.SetMainDicomTag(it->resourceId_, it->tag_, it->value_); + } + } + + for (std::list::const_iterator + it = metadata_.begin(); it != metadata_.end(); ++it) + { + compatibility.SetMetadata(it->resourceId_, it->metadata_, it->value_); + } + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/ResourcesContent.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/ResourcesContent.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,134 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../../Core/DicomFormat/DicomMap.h" +#include "../ServerEnumerations.h" + +#include +#include + + +namespace Orthanc +{ + namespace Compatibility + { + class ISetResourcesContent; + } + + class ResourcesContent : public boost::noncopyable + { + public: + struct TagValue + { + int64_t resourceId_; + bool isIdentifier_; + DicomTag tag_; + std::string value_; + + TagValue(int64_t resourceId, + bool isIdentifier, + const DicomTag& tag, + const std::string& value) : + resourceId_(resourceId), + isIdentifier_(isIdentifier), + tag_(tag), + value_(value) + { + } + }; + + struct Metadata + { + int64_t resourceId_; + MetadataType metadata_; + std::string value_; + + Metadata(int64_t resourceId, + MetadataType metadata, + const std::string& value) : + resourceId_(resourceId), + metadata_(metadata), + value_(value) + { + } + }; + + typedef std::list ListTags; + typedef std::list ListMetadata; + + private: + ListTags tags_; + ListMetadata metadata_; + + public: + void AddMainDicomTag(int64_t resourceId, + const DicomTag& tag, + const std::string& value) + { + tags_.push_back(TagValue(resourceId, false, tag, value)); + } + + void AddIdentifierTag(int64_t resourceId, + const DicomTag& tag, + const std::string& value) + { + tags_.push_back(TagValue(resourceId, true, tag, value)); + } + + void AddMetadata(int64_t resourceId, + MetadataType metadata, + const std::string& value) + { + metadata_.push_back(Metadata(resourceId, metadata, value)); + } + + void AddResource(int64_t resource, + ResourceType level, + const DicomMap& dicomSummary); + + // WARNING: The database should be locked with a transaction! + void Store(Compatibility::ISetResourcesContent& target) const; + + const ListTags& GetListTags() const + { + return tags_; + } + + const ListMetadata& GetListMetadata() const + { + return metadata_; + } + }; +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/SQLiteDatabaseWrapper.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/SQLiteDatabaseWrapper.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,1351 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "SQLiteDatabaseWrapper.h" + +#include "../../Core/DicomFormat/DicomArray.h" +#include "../../Core/Logging.h" +#include "../../Core/SQLite/Transaction.h" +#include "../Search/ISqlLookupFormatter.h" +#include "../ServerToolbox.h" + +#include + +#include +#include + +namespace Orthanc +{ + namespace Internals + { + class SignalFileDeleted : public SQLite::IScalarFunction + { + private: + IDatabaseListener& listener_; + + public: + SignalFileDeleted(IDatabaseListener& listener) : + listener_(listener) + { + } + + virtual const char* GetName() const + { + return "SignalFileDeleted"; + } + + virtual unsigned int GetCardinality() const + { + return 7; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + std::string uncompressedMD5, compressedMD5; + + if (!context.IsNullValue(5)) + { + uncompressedMD5 = context.GetStringValue(5); + } + + if (!context.IsNullValue(6)) + { + compressedMD5 = context.GetStringValue(6); + } + + FileInfo info(context.GetStringValue(0), + static_cast(context.GetIntValue(1)), + static_cast(context.GetInt64Value(2)), + uncompressedMD5, + static_cast(context.GetIntValue(3)), + static_cast(context.GetInt64Value(4)), + compressedMD5); + + listener_.SignalFileDeleted(info); + } + }; + + class SignalResourceDeleted : public SQLite::IScalarFunction + { + private: + IDatabaseListener& listener_; + + public: + SignalResourceDeleted(IDatabaseListener& listener) : + listener_(listener) + { + } + + virtual const char* GetName() const + { + return "SignalResourceDeleted"; + } + + virtual unsigned int GetCardinality() const + { + return 2; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + ResourceType type = static_cast(context.GetIntValue(1)); + ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0)); + listener_.SignalChange(change); + } + }; + + class SignalRemainingAncestor : public SQLite::IScalarFunction + { + private: + bool hasRemainingAncestor_; + std::string remainingPublicId_; + ResourceType remainingType_; + + public: + SignalRemainingAncestor() : + hasRemainingAncestor_(false) + { + } + + void Reset() + { + hasRemainingAncestor_ = false; + } + + virtual const char* GetName() const + { + return "SignalRemainingAncestor"; + } + + virtual unsigned int GetCardinality() const + { + return 2; + } + + virtual void Compute(SQLite::FunctionContext& context) + { + VLOG(1) << "There exists a remaining ancestor with public ID \"" + << context.GetStringValue(0) + << "\" of type " + << context.GetIntValue(1); + + if (!hasRemainingAncestor_ || + remainingType_ >= context.GetIntValue(1)) + { + hasRemainingAncestor_ = true; + remainingPublicId_ = context.GetStringValue(0); + remainingType_ = static_cast(context.GetIntValue(1)); + } + } + + bool HasRemainingAncestor() const + { + return hasRemainingAncestor_; + } + + const std::string& GetRemainingAncestorId() const + { + assert(hasRemainingAncestor_); + return remainingPublicId_; + } + + ResourceType GetRemainingAncestorType() const + { + assert(hasRemainingAncestor_); + return remainingType_; + } + }; + } + + + void SQLiteDatabaseWrapper::GetChangesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults) + { + target.clear(); + + while (target.size() < maxResults && s.Step()) + { + int64_t seq = s.ColumnInt64(0); + ChangeType changeType = static_cast(s.ColumnInt(1)); + ResourceType resourceType = static_cast(s.ColumnInt(3)); + const std::string& date = s.ColumnString(4); + + int64_t internalId = s.ColumnInt64(2); + std::string publicId = GetPublicId(internalId); + + target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date)); + } + + done = !(target.size() == maxResults && s.Step()); + } + + + void SQLiteDatabaseWrapper::GetExportedResourcesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults) + { + target.clear(); + + while (target.size() < maxResults && s.Step()) + { + int64_t seq = s.ColumnInt64(0); + ResourceType resourceType = static_cast(s.ColumnInt(1)); + std::string publicId = s.ColumnString(2); + + ExportedResource resource(seq, + resourceType, + publicId, + s.ColumnString(3), // modality + s.ColumnString(8), // date + s.ColumnString(4), // patient ID + s.ColumnString(5), // study instance UID + s.ColumnString(6), // series instance UID + s.ColumnString(7)); // sop instance UID + + target.push_back(resource); + } + + done = !(target.size() == maxResults && s.Step()); + } + + + void SQLiteDatabaseWrapper::GetChildren(std::list& childrenPublicIds, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?"); + s.BindInt64(0, id); + + childrenPublicIds.clear(); + while (s.Step()) + { + childrenPublicIds.push_back(s.ColumnString(0)); + } + } + + + void SQLiteDatabaseWrapper::DeleteResource(int64_t id) + { + signalRemainingAncestor_->Reset(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?"); + s.BindInt64(0, id); + s.Run(); + + if (signalRemainingAncestor_->HasRemainingAncestor() && + listener_ != NULL) + { + listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(), + signalRemainingAncestor_->GetRemainingAncestorId()); + } + } + + + bool SQLiteDatabaseWrapper::GetParentPublicId(std::string& target, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " + "WHERE a.internalId = b.parentId AND b.internalId = ?"); + s.BindInt64(0, id); + + if (s.Step()) + { + target = s.ColumnString(0); + return true; + } + else + { + return false; + } + } + + + int64_t SQLiteDatabaseWrapper::GetTableRecordCount(const std::string& table) + { + char buf[128]; + sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str()); + SQLite::Statement s(db_, buf); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_InternalError); + } + + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + + return c; + } + + + SQLiteDatabaseWrapper::SQLiteDatabaseWrapper(const std::string& path) : + listener_(NULL), + signalRemainingAncestor_(NULL), + version_(0) + { + db_.Open(path); + } + + + SQLiteDatabaseWrapper::SQLiteDatabaseWrapper() : + listener_(NULL), + signalRemainingAncestor_(NULL), + version_(0) + { + db_.OpenInMemory(); + } + + + int SQLiteDatabaseWrapper::GetGlobalIntegerProperty(GlobalProperty property, + int defaultValue) + { + std::string tmp; + + if (!LookupGlobalProperty(tmp, GlobalProperty_DatabasePatchLevel)) + { + return defaultValue; + } + else + { + try + { + return boost::lexical_cast(tmp); + } + catch (boost::bad_lexical_cast&) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Global property " + boost::lexical_cast(property) + + " should be an integer, but found: " + tmp); + } + } + } + + + void SQLiteDatabaseWrapper::Open() + { + db_.Execute("PRAGMA ENCODING=\"UTF-8\";"); + + // Performance tuning of SQLite with PRAGMAs + // http://www.sqlite.org/pragma.html + db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;"); + db_.Execute("PRAGMA JOURNAL_MODE=WAL;"); + db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;"); + 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;"); + + { + SQLite::Transaction t(db_); + t.Begin(); + + if (!db_.DoesTableExist("GlobalProperties")) + { + LOG(INFO) << "Creating the database"; + std::string query; + EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE); + db_.Execute(query); + } + + // Check the version of the database + std::string tmp; + if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion)) + { + tmp = "Unknown"; + } + + bool ok = false; + try + { + LOG(INFO) << "Version of the Orthanc database: " << tmp; + version_ = boost::lexical_cast(tmp); + ok = true; + } + catch (boost::bad_lexical_cast&) + { + } + + if (!ok) + { + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion, + "Incompatible version of the Orthanc database: " + tmp); + } + + // New in Orthanc 1.5.1 + if (version_ == 6) + { + if (!LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) || + tmp != "1") + { + LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments"; + std::string query; + EmbeddedResources::GetFileResource(query, EmbeddedResources::INSTALL_TRACK_ATTACHMENTS_SIZE); + db_.Execute(query); + } + } + + t.Commit(); + } + + signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; + db_.Register(signalRemainingAncestor_); + } + + + static void ExecuteUpgradeScript(SQLite::Connection& db, + EmbeddedResources::FileResourceId script) + { + std::string upgrade; + EmbeddedResources::GetFileResource(upgrade, script); + db.BeginTransaction(); + db.Execute(upgrade); + db.CommitTransaction(); + } + + + void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion, + IStorageArea& storageArea) + { + if (targetVersion != 6) + { + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); + } + + // This version of Orthanc is only compatible with versions 3, 4, + // 5 and 6 of the DB schema + if (version_ != 3 && + version_ != 4 && + version_ != 5 && + version_ != 6) + { + throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); + } + + if (version_ == 3) + { + LOG(WARNING) << "Upgrading database version from 3 to 4"; + ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_3_TO_4); + version_ = 4; + } + + if (version_ == 4) + { + LOG(WARNING) << "Upgrading database version from 4 to 5"; + ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_4_TO_5); + version_ = 5; + } + + if (version_ == 5) + { + LOG(WARNING) << "Upgrading database version from 5 to 6"; + // No change in the DB schema, the step from version 5 to 6 only + // consists in reconstructing the main DICOM tags information + // (as more tags got included). + db_.BeginTransaction(); + ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Patient); + ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Study); + ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Series); + ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Instance); + db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" + + boost::lexical_cast(GlobalProperty_DatabaseSchemaVersion) + ";"); + db_.CommitTransaction(); + version_ = 6; + } + } + + + void SQLiteDatabaseWrapper::SetListener(IDatabaseListener& listener) + { + listener_ = &listener; + db_.Register(new Internals::SignalFileDeleted(listener)); + db_.Register(new Internals::SignalResourceDeleted(listener)); + } + + + void SQLiteDatabaseWrapper::ClearTable(const std::string& tableName) + { + db_.Execute("DELETE FROM " + tableName); + } + + + bool SQLiteDatabaseWrapper::LookupParent(int64_t& parentId, + int64_t resourceId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT parentId FROM Resources WHERE internalId=?"); + s.BindInt64(0, resourceId); + + if (!s.Step()) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + if (s.ColumnIsNull(0)) + { + return false; + } + else + { + parentId = s.ColumnInt(0); + return true; + } + } + + + ResourceType SQLiteDatabaseWrapper::GetResourceType(int64_t resourceId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT resourceType FROM Resources WHERE internalId=?"); + s.BindInt64(0, resourceId); + + if (s.Step()) + { + return static_cast(s.ColumnInt(0)); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + + std::string SQLiteDatabaseWrapper::GetPublicId(int64_t resourceId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT publicId FROM Resources WHERE internalId=?"); + s.BindInt64(0, resourceId); + + if (s.Step()) + { + return s.ColumnString(0); + } + else + { + throw OrthancException(ErrorCode_UnknownResource); + } + } + + + void SQLiteDatabaseWrapper::GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); + s.BindInt64(0, since); + s.BindInt(1, maxResults + 1); + GetChangesInternal(target, done, s, maxResults); + } + + + void SQLiteDatabaseWrapper::GetLastChange(std::list& target /*out*/) + { + bool done; // Ignored + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1"); + GetChangesInternal(target, done, s, 1); + } + + + class SQLiteDatabaseWrapper::Transaction : public IDatabaseWrapper::ITransaction + { + private: + SQLiteDatabaseWrapper& that_; + std::auto_ptr transaction_; + int64_t initialDiskSize_; + + public: + Transaction(SQLiteDatabaseWrapper& that) : + that_(that), + transaction_(new SQLite::Transaction(that_.db_)) + { +#if defined(NDEBUG) + // Release mode + initialDiskSize_ = 0; +#else + // Debug mode + initialDiskSize_ = static_cast(that_.GetTotalCompressedSize()); +#endif + } + + virtual void Begin() + { + transaction_->Begin(); + } + + virtual void Rollback() + { + transaction_->Rollback(); + } + + virtual void Commit(int64_t fileSizeDelta /* only used in debug */) + { + transaction_->Commit(); + + assert(initialDiskSize_ + fileSizeDelta >= 0 && + initialDiskSize_ + fileSizeDelta == static_cast(that_.GetTotalCompressedSize())); + } + }; + + + IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction() + { + return new Transaction(*this); + } + + + void SQLiteDatabaseWrapper::GetAllMetadata(std::map& target, + int64_t id) + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?"); + s.BindInt64(0, id); + + while (s.Step()) + { + MetadataType key = static_cast(s.ColumnInt(0)); + target[key] = s.ColumnString(1); + } + } + + + void SQLiteDatabaseWrapper::SetGlobalProperty(GlobalProperty property, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); + s.BindInt(0, property); + s.BindString(1, value); + s.Run(); + } + + + bool SQLiteDatabaseWrapper::LookupGlobalProperty(std::string& target, + GlobalProperty property) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM GlobalProperties WHERE property=?"); + s.BindInt(0, property); + + if (!s.Step()) + { + return false; + } + else + { + target = s.ColumnString(0); + return true; + } + } + + + int64_t SQLiteDatabaseWrapper::CreateResource(const std::string& publicId, + ResourceType type) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)"); + s.BindInt(0, type); + s.BindString(1, publicId); + s.Run(); + return db_.GetLastInsertRowId(); + } + + + bool SQLiteDatabaseWrapper::LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT internalId, resourceType FROM Resources WHERE publicId=?"); + s.BindString(0, publicId); + + if (!s.Step()) + { + return false; + } + else + { + id = s.ColumnInt(0); + type = static_cast(s.ColumnInt(1)); + + // Check whether there is a single resource with this public id + assert(!s.Step()); + + return true; + } + } + + + void SQLiteDatabaseWrapper::AttachChild(int64_t parent, + int64_t child) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); + s.BindInt64(0, parent); + s.BindInt64(1, child); + s.Run(); + } + + + void SQLiteDatabaseWrapper::SetMetadata(int64_t id, + MetadataType type, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, type); + s.BindString(2, value); + s.Run(); + } + + + void SQLiteDatabaseWrapper::DeleteMetadata(int64_t id, + MetadataType type) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?"); + s.BindInt64(0, id); + s.BindInt(1, type); + s.Run(); + } + + + bool SQLiteDatabaseWrapper::LookupMetadata(std::string& target, + int64_t id, + MetadataType type) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT value FROM Metadata WHERE id=? AND type=?"); + s.BindInt64(0, id); + s.BindInt(1, type); + + if (!s.Step()) + { + return false; + } + else + { + target = s.ColumnString(0); + return true; + } + } + + + void SQLiteDatabaseWrapper::ListAvailableMetadata(std::list& target, + int64_t id) + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?"); + s.BindInt64(0, id); + + while (s.Step()) + { + target.push_back(static_cast(s.ColumnInt(0))); + } + } + + + void SQLiteDatabaseWrapper::AddAttachment(int64_t id, + const FileInfo& attachment) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, attachment.GetContentType()); + s.BindString(2, attachment.GetUuid()); + s.BindInt64(3, attachment.GetCompressedSize()); + s.BindInt64(4, attachment.GetUncompressedSize()); + s.BindInt(5, attachment.GetCompressionType()); + s.BindString(6, attachment.GetUncompressedMD5()); + s.BindString(7, attachment.GetCompressedMD5()); + s.Run(); + } + + + void SQLiteDatabaseWrapper::DeleteAttachment(int64_t id, + FileContentType attachment) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?"); + s.BindInt64(0, id); + s.BindInt(1, attachment); + s.Run(); + } + + + void SQLiteDatabaseWrapper::ListAvailableAttachments(std::list& target, + int64_t id) + { + target.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT fileType FROM AttachedFiles WHERE id=?"); + s.BindInt64(0, id); + + while (s.Step()) + { + target.push_back(static_cast(s.ColumnInt(0))); + } + } + + bool SQLiteDatabaseWrapper::LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT uuid, uncompressedSize, compressionType, compressedSize, " + "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); + s.BindInt64(0, id); + s.BindInt(1, contentType); + + if (!s.Step()) + { + return false; + } + else + { + attachment = FileInfo(s.ColumnString(0), + contentType, + s.ColumnInt64(1), + s.ColumnString(4), + static_cast(s.ColumnInt(2)), + s.ColumnInt64(3), + s.ColumnString(5)); + return true; + } + } + + + void SQLiteDatabaseWrapper::ClearMainDicomTags(int64_t id) + { + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?"); + s.BindInt64(0, id); + s.Run(); + } + + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?"); + s.BindInt64(0, id); + s.Run(); + } + } + + + void SQLiteDatabaseWrapper::SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, tag.GetGroup()); + s.BindInt(2, tag.GetElement()); + s.BindString(3, value); + s.Run(); + } + + + void SQLiteDatabaseWrapper::SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)"); + s.BindInt64(0, id); + s.BindInt(1, tag.GetGroup()); + s.BindInt(2, tag.GetElement()); + s.BindString(3, value); + s.Run(); + } + + + void SQLiteDatabaseWrapper::GetMainDicomTags(DicomMap& map, + int64_t id) + { + map.Clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?"); + s.BindInt64(0, id); + while (s.Step()) + { + map.SetValue(s.ColumnInt(1), + s.ColumnInt(2), + s.ColumnString(3), false); + } + } + + + void SQLiteDatabaseWrapper::GetChildrenPublicId(std::list& target, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " + "WHERE a.parentId = b.internalId AND b.internalId = ?"); + s.BindInt64(0, id); + + target.clear(); + + while (s.Step()) + { + target.push_back(s.ColumnString(0)); + } + } + + + void SQLiteDatabaseWrapper::GetChildrenInternalId(std::list& target, + int64_t id) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b " + "WHERE a.parentId = b.internalId AND b.internalId = ?"); + s.BindInt64(0, id); + + target.clear(); + + while (s.Step()) + { + target.push_back(s.ColumnInt64(0)); + } + } + + + void SQLiteDatabaseWrapper::LogChange(int64_t internalId, + const ServerIndexChange& change) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); + s.BindInt(0, change.GetChangeType()); + s.BindInt64(1, internalId); + s.BindInt(2, change.GetResourceType()); + s.BindString(3, change.GetDate()); + s.Run(); + } + + + void SQLiteDatabaseWrapper::LogExportedResource(const ExportedResource& resource) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)"); + + s.BindInt(0, resource.GetResourceType()); + s.BindString(1, resource.GetPublicId()); + s.BindString(2, resource.GetModality()); + s.BindString(3, resource.GetPatientId()); + s.BindString(4, resource.GetStudyInstanceUid()); + s.BindString(5, resource.GetSeriesInstanceUid()); + s.BindString(6, resource.GetSopInstanceUid()); + s.BindString(7, resource.GetDate()); + s.Run(); + } + + + void SQLiteDatabaseWrapper::GetExportedResources(std::list& target, + bool& done, + int64_t since, + uint32_t maxResults) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?"); + s.BindInt64(0, since); + s.BindInt(1, maxResults + 1); + GetExportedResourcesInternal(target, done, s, maxResults); + } + + + void SQLiteDatabaseWrapper::GetLastExportedResource(std::list& target) + { + bool done; // Ignored + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1"); + GetExportedResourcesInternal(target, done, s, 1); + } + + + uint64_t SQLiteDatabaseWrapper::GetTotalCompressedSize() + { + // Old SQL query that was used in Orthanc <= 1.5.0: + // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0"); + s.Run(); + return static_cast(s.ColumnInt64(0)); + } + + + uint64_t SQLiteDatabaseWrapper::GetTotalUncompressedSize() + { + // Old SQL query that was used in Orthanc <= 1.5.0: + // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1"); + s.Run(); + return static_cast(s.ColumnInt64(0)); + } + + + uint64_t SQLiteDatabaseWrapper::GetResourceCount(ResourceType resourceType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT COUNT(*) FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + if (!s.Step()) + { + return 0; + } + else + { + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + return c; + } + } + + + void SQLiteDatabaseWrapper::GetAllPublicIds(std::list& target, + ResourceType resourceType) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?"); + s.BindInt(0, resourceType); + + target.clear(); + while (s.Step()) + { + target.push_back(s.ColumnString(0)); + } + } + + + void SQLiteDatabaseWrapper::GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) + { + if (limit == 0) + { + target.clear(); + return; + } + + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT publicId FROM Resources WHERE " + "resourceType=? LIMIT ? OFFSET ?"); + s.BindInt(0, resourceType); + s.BindInt64(1, limit); + s.BindInt64(2, since); + + target.clear(); + while (s.Step()) + { + target.push_back(s.ColumnString(0)); + } + } + + + bool SQLiteDatabaseWrapper::SelectPatientToRecycle(int64_t& internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1"); + + if (!s.Step()) + { + // No patient remaining or all the patients are protected + return false; + } + else + { + internalId = s.ColumnInt(0); + return true; + } + } + + + bool SQLiteDatabaseWrapper::SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT patientId FROM PatientRecyclingOrder " + "WHERE patientId != ? ORDER BY seq ASC LIMIT 1"); + s.BindInt64(0, patientIdToAvoid); + + if (!s.Step()) + { + // No patient remaining or all the patients are protected + return false; + } + else + { + internalId = s.ColumnInt(0); + return true; + } + } + + + bool SQLiteDatabaseWrapper::IsProtectedPatient(int64_t internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?"); + s.BindInt64(0, internalId); + return !s.Step(); + } + + + void SQLiteDatabaseWrapper::SetProtectedPatient(int64_t internalId, + bool isProtected) + { + if (isProtected) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); + s.BindInt64(0, internalId); + s.Run(); + } + else if (IsProtectedPatient(internalId)) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); + s.BindInt64(0, internalId); + s.Run(); + } + else + { + // Nothing to do: The patient is already unprotected + } + } + + + bool SQLiteDatabaseWrapper::IsExistingResource(int64_t internalId) + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT * FROM Resources WHERE internalId=?"); + s.BindInt64(0, internalId); + return s.Step(); + } + + + bool SQLiteDatabaseWrapper::IsDiskSizeAbove(uint64_t threshold) + { + return GetTotalCompressedSize() > threshold; + } + + + + class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter + { + private: + std::list values_; + + public: + virtual std::string GenerateParameter(const std::string& value) + { + values_.push_back(value); + return "?"; + } + + virtual std::string FormatResourceType(ResourceType level) + { + return boost::lexical_cast(level); + } + + virtual std::string FormatWildcardEscape() + { + return "ESCAPE '\\'"; + } + + void Bind(SQLite::Statement& statement) const + { + size_t pos = 0; + + for (std::list::const_iterator + it = values_.begin(); it != values_.end(); ++it, pos++) + { + statement.BindString(pos, *it); + } + } + }; + + + static void AnswerLookup(std::list& resourcesId, + std::list& instancesId, + SQLite::Connection& db, + ResourceType level) + { + resourcesId.clear(); + instancesId.clear(); + + std::auto_ptr statement; + + switch (level) + { + case ResourceType_Patient: + { + statement.reset( + new SQLite::Statement( + db, SQLITE_FROM_HERE, + "SELECT patients.publicId, instances.publicID FROM Lookup AS patients " + "INNER JOIN Resources studies ON patients.internalId=studies.parentId " + "INNER JOIN Resources series ON studies.internalId=series.parentId " + "INNER JOIN Resources instances ON series.internalId=instances.parentId " + "GROUP BY patients.publicId")); + + break; + } + + case ResourceType_Study: + { + statement.reset( + new SQLite::Statement( + db, SQLITE_FROM_HERE, + "SELECT studies.publicId, instances.publicID FROM Lookup AS studies " + "INNER JOIN Resources series ON studies.internalId=series.parentId " + "INNER JOIN Resources instances ON series.internalId=instances.parentId " + "GROUP BY studies.publicId")); + + break; + } + + case ResourceType_Series: + { + statement.reset( + new SQLite::Statement( + db, SQLITE_FROM_HERE, + "SELECT series.publicId, instances.publicID FROM Lookup AS series " + "INNER JOIN Resources instances ON series.internalId=instances.parentId " + "GROUP BY series.publicId")); + + break; + } + + case ResourceType_Instance: + { + statement.reset( + new SQLite::Statement( + db, SQLITE_FROM_HERE, "SELECT publicId, publicId FROM Lookup")); + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + assert(statement.get() != NULL); + + while (statement->Step()) + { + resourcesId.push_back(statement->ColumnString(0)); + instancesId.push_back(statement->ColumnString(1)); + } + } + + + void SQLiteDatabaseWrapper::ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) + { + LookupFormatter formatter; + + std::string sql; + LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit); + + sql = "CREATE TEMPORARY TABLE Lookup AS " + sql; + + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup"); + s.Run(); + } + + { + SQLite::Statement statement(db_, sql); + formatter.Bind(statement); + statement.Run(); + } + + if (instancesId != NULL) + { + AnswerLookup(resourcesId, *instancesId, db_, queryLevel); + } + else + { + resourcesId.clear(); + + SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Lookup"); + + while (s.Step()) + { + resourcesId.push_back(s.ColumnString(0)); + } + } + } + + + int64_t SQLiteDatabaseWrapper::GetLastChangeIndex() + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "SELECT seq FROM sqlite_sequence WHERE name='Changes'"); + + if (s.Step()) + { + int64_t c = s.ColumnInt(0); + assert(!s.Step()); + return c; + } + else + { + // No change has been recorded so far in the database + return 0; + } + } + + + void SQLiteDatabaseWrapper::TagMostRecentPatient(int64_t patient) + { + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); + s.BindInt64(0, patient); + s.Run(); + + assert(db_.GetLastChangeCount() == 0 || + db_.GetLastChangeCount() == 1); + + if (db_.GetLastChangeCount() == 0) + { + // The patient was protected, there was nothing to delete from the recycling order + return; + } + } + + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, + "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); + s.BindInt64(0, patient); + s.Run(); + } + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/SQLiteDatabaseWrapper.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/SQLiteDatabaseWrapper.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,365 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "IDatabaseWrapper.h" + +#include "../../Core/SQLite/Connection.h" +#include "Compatibility/ICreateInstance.h" +#include "Compatibility/IGetChildrenMetadata.h" +#include "Compatibility/ISetResourcesContent.h" + +namespace Orthanc +{ + namespace Internals + { + class SignalRemainingAncestor; + } + + /** + * This class manages an instance of the Orthanc SQLite database. It + * translates low-level requests into SQL statements. Mutual + * exclusion MUST be implemented at a higher level. + **/ + class SQLiteDatabaseWrapper : + public IDatabaseWrapper, + public Compatibility::ICreateInstance, + public Compatibility::IGetChildrenMetadata, + public Compatibility::ISetResourcesContent + { + private: + class Transaction; + class LookupFormatter; + + IDatabaseListener* listener_; + SQLite::Connection db_; + Internals::SignalRemainingAncestor* signalRemainingAncestor_; + unsigned int version_; + + void GetChangesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults); + + void GetExportedResourcesInternal(std::list& target, + bool& done, + SQLite::Statement& s, + uint32_t maxResults); + + void ClearTable(const std::string& tableName); + + // Unused => could be removed + int GetGlobalIntegerProperty(GlobalProperty property, + int defaultValue); + + public: + SQLiteDatabaseWrapper(const std::string& path); + + SQLiteDatabaseWrapper(); + + virtual void Open() + ORTHANC_OVERRIDE; + + virtual void Close() + ORTHANC_OVERRIDE + { + db_.Close(); + } + + virtual void SetListener(IDatabaseListener& listener) + ORTHANC_OVERRIDE; + + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId) + ORTHANC_OVERRIDE; + + virtual std::string GetPublicId(int64_t resourceId) + ORTHANC_OVERRIDE; + + virtual ResourceType GetResourceType(int64_t resourceId) + ORTHANC_OVERRIDE; + + virtual void DeleteResource(int64_t id) + ORTHANC_OVERRIDE; + + virtual void GetChanges(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) + ORTHANC_OVERRIDE; + + virtual void GetLastChange(std::list& target /*out*/) + ORTHANC_OVERRIDE; + + virtual IDatabaseWrapper::ITransaction* StartTransaction() + ORTHANC_OVERRIDE; + + virtual void FlushToDisk() + ORTHANC_OVERRIDE + { + db_.FlushToDisk(); + } + + virtual bool HasFlushToDisk() const + ORTHANC_OVERRIDE + { + return true; + } + + virtual void ClearChanges() + ORTHANC_OVERRIDE + { + ClearTable("Changes"); + } + + virtual void ClearExportedResources() + ORTHANC_OVERRIDE + { + ClearTable("ExportedResources"); + } + + virtual void GetAllMetadata(std::map& target, + int64_t id) + ORTHANC_OVERRIDE; + + virtual unsigned int GetDatabaseVersion() + ORTHANC_OVERRIDE + { + return version_; + } + + virtual void Upgrade(unsigned int targetVersion, + IStorageArea& storageArea) + ORTHANC_OVERRIDE; + + + /** + * The methods declared below are for unit testing only! + **/ + + const char* GetErrorMessage() const + { + return db_.GetErrorMessage(); + } + + void GetChildren(std::list& childrenPublicIds, + int64_t id); + + int64_t GetTableRecordCount(const std::string& table); + + bool GetParentPublicId(std::string& target, + int64_t id); + + + + /** + * Until Orthanc 1.4.0, the methods below were part of the + * "DatabaseWrapperBase" class, that is now placed in the + * graveyard. + **/ + + virtual void SetGlobalProperty(GlobalProperty property, + const std::string& value) + ORTHANC_OVERRIDE; + + virtual bool LookupGlobalProperty(std::string& target, + GlobalProperty property) + ORTHANC_OVERRIDE; + + virtual int64_t CreateResource(const std::string& publicId, + ResourceType type) + ORTHANC_OVERRIDE; + + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) + ORTHANC_OVERRIDE; + + virtual void AttachChild(int64_t parent, + int64_t child) + ORTHANC_OVERRIDE; + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value) + ORTHANC_OVERRIDE; + + virtual void DeleteMetadata(int64_t id, + MetadataType type) + ORTHANC_OVERRIDE; + + virtual bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type) + ORTHANC_OVERRIDE; + + virtual void ListAvailableMetadata(std::list& target, + int64_t id) + ORTHANC_OVERRIDE; + + virtual void AddAttachment(int64_t id, + const FileInfo& attachment) + ORTHANC_OVERRIDE; + + virtual void DeleteAttachment(int64_t id, + FileContentType attachment) + ORTHANC_OVERRIDE; + + virtual void ListAvailableAttachments(std::list& target, + int64_t id) + ORTHANC_OVERRIDE; + + virtual bool LookupAttachment(FileInfo& attachment, + int64_t id, + FileContentType contentType) + ORTHANC_OVERRIDE; + + virtual void ClearMainDicomTags(int64_t id) + ORTHANC_OVERRIDE; + + virtual void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) + ORTHANC_OVERRIDE; + + virtual void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) + ORTHANC_OVERRIDE; + + virtual void GetMainDicomTags(DicomMap& map, + int64_t id) + ORTHANC_OVERRIDE; + + virtual void GetChildrenPublicId(std::list& target, + int64_t id) + ORTHANC_OVERRIDE; + + virtual void GetChildrenInternalId(std::list& target, + int64_t id) + ORTHANC_OVERRIDE; + + virtual void LogChange(int64_t internalId, + const ServerIndexChange& change) + ORTHANC_OVERRIDE; + + virtual void LogExportedResource(const ExportedResource& resource) + ORTHANC_OVERRIDE; + + virtual void GetExportedResources(std::list& target /*out*/, + bool& done /*out*/, + int64_t since, + uint32_t maxResults) + ORTHANC_OVERRIDE; + + virtual void GetLastExportedResource(std::list& target /*out*/) + ORTHANC_OVERRIDE; + + virtual uint64_t GetTotalCompressedSize() + ORTHANC_OVERRIDE; + + virtual uint64_t GetTotalUncompressedSize() + ORTHANC_OVERRIDE; + + virtual uint64_t GetResourceCount(ResourceType resourceType) + ORTHANC_OVERRIDE; + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType) + ORTHANC_OVERRIDE; + + virtual void GetAllPublicIds(std::list& target, + ResourceType resourceType, + size_t since, + size_t limit) + ORTHANC_OVERRIDE; + + virtual bool SelectPatientToRecycle(int64_t& internalId) + ORTHANC_OVERRIDE; + + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) + ORTHANC_OVERRIDE; + + virtual bool IsProtectedPatient(int64_t internalId) + ORTHANC_OVERRIDE; + + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected) + ORTHANC_OVERRIDE; + + virtual bool IsExistingResource(int64_t internalId) + ORTHANC_OVERRIDE; + + virtual bool IsDiskSizeAbove(uint64_t threshold) + ORTHANC_OVERRIDE; + + virtual void ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) + ORTHANC_OVERRIDE; + + virtual bool CreateInstance(CreateInstanceResult& result, + int64_t& instanceId, + const std::string& patient, + const std::string& study, + const std::string& series, + const std::string& instance) + ORTHANC_OVERRIDE + { + return ICreateInstance::Apply + (*this, result, instanceId, patient, study, series, instance); + } + + virtual void SetResourcesContent(const Orthanc::ResourcesContent& content) + ORTHANC_OVERRIDE + { + ISetResourcesContent::Apply(*this, content); + } + + virtual void GetChildrenMetadata(std::list& target, + int64_t resourceId, + MetadataType metadata) + ORTHANC_OVERRIDE + { + IGetChildrenMetadata::Apply(*this, target, resourceId, metadata); + } + + virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE; + + virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE; + }; +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Upgrade3To4.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Upgrade3To4.sql Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,24 @@ +-- This SQLite script updates the version of the Orthanc database from 3 to 4. + +-- Add 2 new columns at "AttachedFiles" + +ALTER TABLE AttachedFiles ADD COLUMN uncompressedMD5 TEXT; +ALTER TABLE AttachedFiles ADD COLUMN compressedMD5 TEXT; + +-- Update the "AttachedFileDeleted" trigger + +DROP TRIGGER AttachedFileDeleted; + +CREATE TRIGGER AttachedFileDeleted +AFTER DELETE ON AttachedFiles +BEGIN + SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, + old.compressionType, old.compressedSize, + -- These 2 arguments are new in Orthanc 0.7.3 (database v4) + old.uncompressedMD5, old.compressedMD5); +END; + +-- Change the database version +-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration + +UPDATE GlobalProperties SET value="4" WHERE property=1; diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Database/Upgrade4To5.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Database/Upgrade4To5.sql Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,66 @@ +-- This SQLite script updates the version of the Orthanc database from 4 to 5. + + +-- Remove 2 indexes to speed up + +DROP INDEX MainDicomTagsIndex2; +DROP INDEX MainDicomTagsIndexValues; + + +-- Add a new table to index the DICOM identifiers + +CREATE TABLE DicomIdentifiers( + id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, + tagGroup INTEGER, + tagElement INTEGER, + value TEXT, + PRIMARY KEY(id, tagGroup, tagElement) + ); + +CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); +CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement); +CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY); + + +-- Migrate data from MainDicomTags to MainResourcesTags and MainInstancesTags + +INSERT INTO DicomIdentifiers SELECT * FROM MainDicomTags + WHERE ((tagGroup = 16 AND tagElement = 32) OR -- PatientID (0x0010, 0x0020) + (tagGroup = 32 AND tagElement = 13) OR -- StudyInstanceUID (0x0020, 0x000d) + (tagGroup = 8 AND tagElement = 80) OR -- AccessionNumber (0x0008, 0x0050) + (tagGroup = 32 AND tagElement = 14) OR -- SeriesInstanceUID (0x0020, 0x000e) + (tagGroup = 8 AND tagElement = 24)); -- SOPInstanceUID (0x0008, 0x0018) + +DELETE FROM MainDicomTags + WHERE ((tagGroup = 16 AND tagElement = 32) OR -- PatientID (0x0010, 0x0020) + (tagGroup = 32 AND tagElement = 13) OR -- StudyInstanceUID (0x0020, 0x000d) + (tagGroup = 8 AND tagElement = 80) OR -- AccessionNumber (0x0008, 0x0050) + (tagGroup = 32 AND tagElement = 14) OR -- SeriesInstanceUID (0x0020, 0x000e) + (tagGroup = 8 AND tagElement = 24)); -- SOPInstanceUID (0x0008, 0x0018) + + +-- Upgrade the "ResourceDeleted" trigger + +DROP TRIGGER ResourceDeleted; +DROP TRIGGER ResourceDeletedParentCleaning; + +CREATE TRIGGER ResourceDeleted +AFTER DELETE ON Resources +BEGIN + SELECT SignalResourceDeleted(old.publicId, old.resourceType); + SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) + FROM Resources AS parent WHERE internalId = old.parentId; +END; + +CREATE TRIGGER ResourceDeletedParentCleaning +AFTER DELETE ON Resources +FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0 +BEGIN + DELETE FROM Resources WHERE internalId = old.parentId; +END; + + +-- Change the database version +-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration + +UPDATE GlobalProperties SET value="5" WHERE property=1; diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/DatabaseWrapper.cpp --- a/OrthancServer/DatabaseWrapper.cpp Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1131 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "DatabaseWrapper.h" - -#include "../Core/DicomFormat/DicomArray.h" -#include "../Core/Logging.h" -#include "EmbeddedResources.h" -#include "ServerToolbox.h" - -#include -#include - -namespace Orthanc -{ - namespace Internals - { - class SignalFileDeleted : public SQLite::IScalarFunction - { - private: - IDatabaseListener& listener_; - - public: - SignalFileDeleted(IDatabaseListener& listener) : - listener_(listener) - { - } - - virtual const char* GetName() const - { - return "SignalFileDeleted"; - } - - virtual unsigned int GetCardinality() const - { - return 7; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - std::string uncompressedMD5, compressedMD5; - - if (!context.IsNullValue(5)) - { - uncompressedMD5 = context.GetStringValue(5); - } - - if (!context.IsNullValue(6)) - { - compressedMD5 = context.GetStringValue(6); - } - - FileInfo info(context.GetStringValue(0), - static_cast(context.GetIntValue(1)), - static_cast(context.GetInt64Value(2)), - uncompressedMD5, - static_cast(context.GetIntValue(3)), - static_cast(context.GetInt64Value(4)), - compressedMD5); - - listener_.SignalFileDeleted(info); - } - }; - - class SignalResourceDeleted : public SQLite::IScalarFunction - { - private: - IDatabaseListener& listener_; - - public: - SignalResourceDeleted(IDatabaseListener& listener) : - listener_(listener) - { - } - - virtual const char* GetName() const - { - return "SignalResourceDeleted"; - } - - virtual unsigned int GetCardinality() const - { - return 2; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - ResourceType type = static_cast(context.GetIntValue(1)); - ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0)); - listener_.SignalChange(change); - } - }; - - class SignalRemainingAncestor : public SQLite::IScalarFunction - { - private: - bool hasRemainingAncestor_; - std::string remainingPublicId_; - ResourceType remainingType_; - - public: - SignalRemainingAncestor() : - hasRemainingAncestor_(false) - { - } - - void Reset() - { - hasRemainingAncestor_ = false; - } - - virtual const char* GetName() const - { - return "SignalRemainingAncestor"; - } - - virtual unsigned int GetCardinality() const - { - return 2; - } - - virtual void Compute(SQLite::FunctionContext& context) - { - VLOG(1) << "There exists a remaining ancestor with public ID \"" - << context.GetStringValue(0) - << "\" of type " - << context.GetIntValue(1); - - if (!hasRemainingAncestor_ || - remainingType_ >= context.GetIntValue(1)) - { - hasRemainingAncestor_ = true; - remainingPublicId_ = context.GetStringValue(0); - remainingType_ = static_cast(context.GetIntValue(1)); - } - } - - bool HasRemainingAncestor() const - { - return hasRemainingAncestor_; - } - - const std::string& GetRemainingAncestorId() const - { - assert(hasRemainingAncestor_); - return remainingPublicId_; - } - - ResourceType GetRemainingAncestorType() const - { - assert(hasRemainingAncestor_); - return remainingType_; - } - }; - } - - - void DatabaseWrapper::GetChangesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults) - { - target.clear(); - - while (target.size() < maxResults && s.Step()) - { - int64_t seq = s.ColumnInt64(0); - ChangeType changeType = static_cast(s.ColumnInt(1)); - ResourceType resourceType = static_cast(s.ColumnInt(3)); - const std::string& date = s.ColumnString(4); - - int64_t internalId = s.ColumnInt64(2); - std::string publicId = GetPublicId(internalId); - - target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date)); - } - - done = !(target.size() == maxResults && s.Step()); - } - - - void DatabaseWrapper::GetExportedResourcesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults) - { - target.clear(); - - while (target.size() < maxResults && s.Step()) - { - int64_t seq = s.ColumnInt64(0); - ResourceType resourceType = static_cast(s.ColumnInt(1)); - std::string publicId = s.ColumnString(2); - - ExportedResource resource(seq, - resourceType, - publicId, - s.ColumnString(3), // modality - s.ColumnString(8), // date - s.ColumnString(4), // patient ID - s.ColumnString(5), // study instance UID - s.ColumnString(6), // series instance UID - s.ColumnString(7)); // sop instance UID - - target.push_back(resource); - } - - done = !(target.size() == maxResults && s.Step()); - } - - - void DatabaseWrapper::GetChildren(std::list& childrenPublicIds, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?"); - s.BindInt64(0, id); - - childrenPublicIds.clear(); - while (s.Step()) - { - childrenPublicIds.push_back(s.ColumnString(0)); - } - } - - - void DatabaseWrapper::DeleteResource(int64_t id) - { - signalRemainingAncestor_->Reset(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?"); - s.BindInt64(0, id); - s.Run(); - - if (signalRemainingAncestor_->HasRemainingAncestor() && - listener_ != NULL) - { - listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(), - signalRemainingAncestor_->GetRemainingAncestorId()); - } - } - - - bool DatabaseWrapper::GetParentPublicId(std::string& target, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " - "WHERE a.internalId = b.parentId AND b.internalId = ?"); - s.BindInt64(0, id); - - if (s.Step()) - { - target = s.ColumnString(0); - return true; - } - else - { - return false; - } - } - - - int64_t DatabaseWrapper::GetTableRecordCount(const std::string& table) - { - char buf[128]; - sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str()); - SQLite::Statement s(db_, buf); - - if (!s.Step()) - { - throw OrthancException(ErrorCode_InternalError); - } - - int64_t c = s.ColumnInt(0); - assert(!s.Step()); - - return c; - } - - - DatabaseWrapper::DatabaseWrapper(const std::string& path) : - listener_(NULL), - signalRemainingAncestor_(NULL), - version_(0) - { - db_.Open(path); - } - - - DatabaseWrapper::DatabaseWrapper() : - listener_(NULL), - signalRemainingAncestor_(NULL), - version_(0) - { - db_.OpenInMemory(); - } - - - void DatabaseWrapper::Open() - { - db_.Execute("PRAGMA ENCODING=\"UTF-8\";"); - - // Performance tuning of SQLite with PRAGMAs - // http://www.sqlite.org/pragma.html - db_.Execute("PRAGMA SYNCHRONOUS=NORMAL;"); - db_.Execute("PRAGMA JOURNAL_MODE=WAL;"); - db_.Execute("PRAGMA LOCKING_MODE=EXCLUSIVE;"); - db_.Execute("PRAGMA WAL_AUTOCHECKPOINT=1000;"); - //db_.Execute("PRAGMA TEMP_STORE=memory"); - - if (!db_.DoesTableExist("GlobalProperties")) - { - LOG(INFO) << "Creating the database"; - std::string query; - EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE); - db_.Execute(query); - } - - // Check the version of the database - std::string tmp; - if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion)) - { - tmp = "Unknown"; - } - - bool ok = false; - try - { - LOG(INFO) << "Version of the Orthanc database: " << tmp; - version_ = boost::lexical_cast(tmp); - ok = true; - } - catch (boost::bad_lexical_cast&) - { - } - - if (!ok) - { - throw OrthancException(ErrorCode_IncompatibleDatabaseVersion, - "Incompatible version of the Orthanc database: " + tmp); - } - - signalRemainingAncestor_ = new Internals::SignalRemainingAncestor; - db_.Register(signalRemainingAncestor_); - } - - - static void ExecuteUpgradeScript(SQLite::Connection& db, - EmbeddedResources::FileResourceId script) - { - std::string upgrade; - EmbeddedResources::GetFileResource(upgrade, script); - db.BeginTransaction(); - db.Execute(upgrade); - db.CommitTransaction(); - } - - - void DatabaseWrapper::Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) - { - if (targetVersion != 6) - { - throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); - } - - // This version of Orthanc is only compatible with versions 3, 4, - // 5 and 6 of the DB schema - if (version_ != 3 && - version_ != 4 && - version_ != 5 && - version_ != 6) - { - throw OrthancException(ErrorCode_IncompatibleDatabaseVersion); - } - - if (version_ == 3) - { - LOG(WARNING) << "Upgrading database version from 3 to 4"; - ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_3_TO_4); - version_ = 4; - } - - if (version_ == 4) - { - LOG(WARNING) << "Upgrading database version from 4 to 5"; - ExecuteUpgradeScript(db_, EmbeddedResources::UPGRADE_DATABASE_4_TO_5); - version_ = 5; - } - - if (version_ == 5) - { - LOG(WARNING) << "Upgrading database version from 5 to 6"; - // No change in the DB schema, the step from version 5 to 6 only - // consists in reconstructing the main DICOM tags information - // (as more tags got included). - db_.BeginTransaction(); - ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Patient); - ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Study); - ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Series); - ServerToolbox::ReconstructMainDicomTags(*this, storageArea, ResourceType_Instance); - db_.Execute("UPDATE GlobalProperties SET value=\"6\" WHERE property=" + - boost::lexical_cast(GlobalProperty_DatabaseSchemaVersion) + ";"); - db_.CommitTransaction(); - version_ = 6; - } - } - - - void DatabaseWrapper::SetListener(IDatabaseListener& listener) - { - listener_ = &listener; - db_.Register(new Internals::SignalFileDeleted(listener)); - db_.Register(new Internals::SignalResourceDeleted(listener)); - } - - - void DatabaseWrapper::ClearTable(const std::string& tableName) - { - db_.Execute("DELETE FROM " + tableName); - } - - - bool DatabaseWrapper::LookupParent(int64_t& parentId, - int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT parentId FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (!s.Step()) - { - throw OrthancException(ErrorCode_UnknownResource); - } - - if (s.ColumnIsNull(0)) - { - return false; - } - else - { - parentId = s.ColumnInt(0); - return true; - } - } - - - ResourceType DatabaseWrapper::GetResourceType(int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT resourceType FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (s.Step()) - { - return static_cast(s.ColumnInt(0)); - } - else - { - throw OrthancException(ErrorCode_UnknownResource); - } - } - - - std::string DatabaseWrapper::GetPublicId(int64_t resourceId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT publicId FROM Resources WHERE internalId=?"); - s.BindInt64(0, resourceId); - - if (s.Step()) - { - return s.ColumnString(0); - } - else - { - throw OrthancException(ErrorCode_UnknownResource); - } - } - - - void DatabaseWrapper::GetChanges(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?"); - s.BindInt64(0, since); - s.BindInt(1, maxResults + 1); - GetChangesInternal(target, done, s, maxResults); - } - - - void DatabaseWrapper::GetLastChange(std::list& target /*out*/) - { - bool done; // Ignored - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1"); - GetChangesInternal(target, done, s, 1); - } - - - void DatabaseWrapper::GetAllMetadata(std::map& target, - int64_t id) - { - target.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?"); - s.BindInt64(0, id); - - while (s.Step()) - { - MetadataType key = static_cast(s.ColumnInt(0)); - target[key] = s.ColumnString(1); - } - } - - - void DatabaseWrapper::SetGlobalProperty(GlobalProperty property, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); - s.BindInt(0, property); - s.BindString(1, value); - s.Run(); - } - - - bool DatabaseWrapper::LookupGlobalProperty(std::string& target, - GlobalProperty property) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM GlobalProperties WHERE property=?"); - s.BindInt(0, property); - - if (!s.Step()) - { - return false; - } - else - { - target = s.ColumnString(0); - return true; - } - } - - - int64_t DatabaseWrapper::CreateResource(const std::string& publicId, - ResourceType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)"); - s.BindInt(0, type); - s.BindString(1, publicId); - s.Run(); - return db_.GetLastInsertRowId(); - } - - - bool DatabaseWrapper::LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT internalId, resourceType FROM Resources WHERE publicId=?"); - s.BindString(0, publicId); - - if (!s.Step()) - { - return false; - } - else - { - id = s.ColumnInt(0); - type = static_cast(s.ColumnInt(1)); - - // Check whether there is a single resource with this public id - assert(!s.Step()); - - return true; - } - } - - - void DatabaseWrapper::AttachChild(int64_t parent, - int64_t child) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); - s.BindInt64(0, parent); - s.BindInt64(1, child); - s.Run(); - } - - - void DatabaseWrapper::SetMetadata(int64_t id, - MetadataType type, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, type); - s.BindString(2, value); - s.Run(); - } - - - void DatabaseWrapper::DeleteMetadata(int64_t id, - MetadataType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Metadata WHERE id=? and type=?"); - s.BindInt64(0, id); - s.BindInt(1, type); - s.Run(); - } - - - bool DatabaseWrapper::LookupMetadata(std::string& target, - int64_t id, - MetadataType type) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT value FROM Metadata WHERE id=? AND type=?"); - s.BindInt64(0, id); - s.BindInt(1, type); - - if (!s.Step()) - { - return false; - } - else - { - target = s.ColumnString(0); - return true; - } - } - - - void DatabaseWrapper::ListAvailableMetadata(std::list& target, - int64_t id) - { - target.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type FROM Metadata WHERE id=?"); - s.BindInt64(0, id); - - while (s.Step()) - { - target.push_back(static_cast(s.ColumnInt(0))); - } - } - - - void DatabaseWrapper::AddAttachment(int64_t id, - const FileInfo& attachment) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?, ?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, attachment.GetContentType()); - s.BindString(2, attachment.GetUuid()); - s.BindInt64(3, attachment.GetCompressedSize()); - s.BindInt64(4, attachment.GetUncompressedSize()); - s.BindInt(5, attachment.GetCompressionType()); - s.BindString(6, attachment.GetUncompressedMD5()); - s.BindString(7, attachment.GetCompressedMD5()); - s.Run(); - } - - - void DatabaseWrapper::DeleteAttachment(int64_t id, - FileContentType attachment) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM AttachedFiles WHERE id=? AND fileType=?"); - s.BindInt64(0, id); - s.BindInt(1, attachment); - s.Run(); - } - - - void DatabaseWrapper::ListAvailableAttachments(std::list& target, - int64_t id) - { - target.clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT fileType FROM AttachedFiles WHERE id=?"); - s.BindInt64(0, id); - - while (s.Step()) - { - target.push_back(static_cast(s.ColumnInt(0))); - } - } - - bool DatabaseWrapper::LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT uuid, uncompressedSize, compressionType, compressedSize, " - "uncompressedMD5, compressedMD5 FROM AttachedFiles WHERE id=? AND fileType=?"); - s.BindInt64(0, id); - s.BindInt(1, contentType); - - if (!s.Step()) - { - return false; - } - else - { - attachment = FileInfo(s.ColumnString(0), - contentType, - s.ColumnInt64(1), - s.ColumnString(4), - static_cast(s.ColumnInt(2)), - s.ColumnInt64(3), - s.ColumnString(5)); - return true; - } - } - - - void DatabaseWrapper::ClearMainDicomTags(int64_t id) - { - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM DicomIdentifiers WHERE id=?"); - s.BindInt64(0, id); - s.Run(); - } - - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM MainDicomTags WHERE id=?"); - s.BindInt64(0, id); - s.Run(); - } - } - - - void DatabaseWrapper::SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, tag.GetGroup()); - s.BindInt(2, tag.GetElement()); - s.BindString(3, value); - s.Run(); - } - - - void DatabaseWrapper::SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)"); - s.BindInt64(0, id); - s.BindInt(1, tag.GetGroup()); - s.BindInt(2, tag.GetElement()); - s.BindString(3, value); - s.Run(); - } - - - void DatabaseWrapper::GetMainDicomTags(DicomMap& map, - int64_t id) - { - map.Clear(); - - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?"); - s.BindInt64(0, id); - while (s.Step()) - { - map.SetValue(s.ColumnInt(1), - s.ColumnInt(2), - s.ColumnString(3), false); - } - } - - - void DatabaseWrapper::GetChildrenPublicId(std::list& target, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " - "WHERE a.parentId = b.internalId AND b.internalId = ?"); - s.BindInt64(0, id); - - target.clear(); - - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - void DatabaseWrapper::GetChildrenInternalId(std::list& target, - int64_t id) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b " - "WHERE a.parentId = b.internalId AND b.internalId = ?"); - s.BindInt64(0, id); - - target.clear(); - - while (s.Step()) - { - target.push_back(s.ColumnInt64(0)); - } - } - - - void DatabaseWrapper::LogChange(int64_t internalId, - const ServerIndexChange& change) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)"); - s.BindInt(0, change.GetChangeType()); - s.BindInt64(1, internalId); - s.BindInt(2, change.GetResourceType()); - s.BindString(3, change.GetDate()); - s.Run(); - } - - - void DatabaseWrapper::LogExportedResource(const ExportedResource& resource) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)"); - - s.BindInt(0, resource.GetResourceType()); - s.BindString(1, resource.GetPublicId()); - s.BindString(2, resource.GetModality()); - s.BindString(3, resource.GetPatientId()); - s.BindString(4, resource.GetStudyInstanceUid()); - s.BindString(5, resource.GetSeriesInstanceUid()); - s.BindString(6, resource.GetSopInstanceUid()); - s.BindString(7, resource.GetDate()); - s.Run(); - } - - - void DatabaseWrapper::GetExportedResources(std::list& target, - bool& done, - int64_t since, - uint32_t maxResults) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?"); - s.BindInt64(0, since); - s.BindInt(1, maxResults + 1); - GetExportedResourcesInternal(target, done, s, maxResults); - } - - - void DatabaseWrapper::GetLastExportedResource(std::list& target) - { - bool done; // Ignored - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1"); - GetExportedResourcesInternal(target, done, s, 1); - } - - - uint64_t DatabaseWrapper::GetTotalCompressedSize() - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles"); - s.Run(); - return static_cast(s.ColumnInt64(0)); - } - - - uint64_t DatabaseWrapper::GetTotalUncompressedSize() - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles"); - s.Run(); - return static_cast(s.ColumnInt64(0)); - } - - - uint64_t DatabaseWrapper::GetResourceCount(ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT COUNT(*) FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - if (!s.Step()) - { - return 0; - } - else - { - int64_t c = s.ColumnInt(0); - assert(!s.Step()); - return c; - } - } - - - void DatabaseWrapper::GetAllInternalIds(std::list& target, - ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT internalId FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnInt64(0)); - } - } - - - void DatabaseWrapper::GetAllPublicIds(std::list& target, - ResourceType resourceType) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?"); - s.BindInt(0, resourceType); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - void DatabaseWrapper::GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit) - { - if (limit == 0) - { - target.clear(); - return; - } - - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT publicId FROM Resources WHERE " - "resourceType=? LIMIT ? OFFSET ?"); - s.BindInt(0, resourceType); - s.BindInt64(1, limit); - s.BindInt64(2, since); - - target.clear(); - while (s.Step()) - { - target.push_back(s.ColumnString(0)); - } - } - - - bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT patientId FROM PatientRecyclingOrder ORDER BY seq ASC LIMIT 1"); - - if (!s.Step()) - { - // No patient remaining or all the patients are protected - return false; - } - else - { - internalId = s.ColumnInt(0); - return true; - } - } - - - bool DatabaseWrapper::SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT patientId FROM PatientRecyclingOrder " - "WHERE patientId != ? ORDER BY seq ASC LIMIT 1"); - s.BindInt64(0, patientIdToAvoid); - - if (!s.Step()) - { - // No patient remaining or all the patients are protected - return false; - } - else - { - internalId = s.ColumnInt(0); - return true; - } - } - - - bool DatabaseWrapper::IsProtectedPatient(int64_t internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?"); - s.BindInt64(0, internalId); - return !s.Step(); - } - - - void DatabaseWrapper::SetProtectedPatient(int64_t internalId, - bool isProtected) - { - if (isProtected) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM PatientRecyclingOrder WHERE patientId=?"); - s.BindInt64(0, internalId); - s.Run(); - } - else if (IsProtectedPatient(internalId)) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)"); - s.BindInt64(0, internalId); - s.Run(); - } - else - { - // Nothing to do: The patient is already unprotected - } - } - - - bool DatabaseWrapper::IsExistingResource(int64_t internalId) - { - SQLite::Statement s(db_, SQLITE_FROM_HERE, - "SELECT * FROM Resources WHERE internalId=?"); - s.BindInt64(0, internalId); - return s.Step(); - } - - - void DatabaseWrapper::LookupIdentifier(std::list& target, - ResourceType level, - const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value) - { - static const char* COMMON = ("SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE " - "d.id = r.internalId AND r.resourceType=? AND " - "d.tagGroup=? AND d.tagElement=? AND "); - - std::auto_ptr s; - - switch (type) - { - case IdentifierConstraintType_GreaterOrEqual: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value>=?")); - break; - - case IdentifierConstraintType_SmallerOrEqual: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value<=?")); - break; - - case IdentifierConstraintType_Wildcard: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value GLOB ?")); - break; - - case IdentifierConstraintType_Equal: - default: - s.reset(new SQLite::Statement(db_, std::string(COMMON) + "d.value=?")); - break; - } - - assert(s.get() != NULL); - - s->BindInt(0, level); - s->BindInt(1, tag.GetGroup()); - s->BindInt(2, tag.GetElement()); - s->BindString(3, value); - - target.clear(); - - while (s->Step()) - { - target.push_back(s->ColumnInt64(0)); - } - } - - - void DatabaseWrapper::LookupIdentifierRange(std::list& target, - ResourceType level, - const DicomTag& tag, - const std::string& start, - const std::string& end) - { - SQLite::Statement statement(db_, SQLITE_FROM_HERE, - "SELECT d.id FROM DicomIdentifiers AS d, Resources AS r WHERE " - "d.id = r.internalId AND r.resourceType=? AND " - "d.tagGroup=? AND d.tagElement=? AND d.value>=? AND d.value<=?"); - - statement.BindInt(0, level); - statement.BindInt(1, tag.GetGroup()); - statement.BindInt(2, tag.GetElement()); - statement.BindString(3, start); - statement.BindString(4, end); - - target.clear(); - - while (statement.Step()) - { - target.push_back(statement.ColumnInt64(0)); - } - } -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/DatabaseWrapper.h --- a/OrthancServer/DatabaseWrapper.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,280 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "IDatabaseWrapper.h" - -#include "../Core/SQLite/Connection.h" -#include "../Core/SQLite/Transaction.h" - -namespace Orthanc -{ - namespace Internals - { - class SignalRemainingAncestor; - } - - /** - * This class manages an instance of the Orthanc SQLite database. It - * translates low-level requests into SQL statements. Mutual - * exclusion MUST be implemented at a higher level. - **/ - class DatabaseWrapper : public IDatabaseWrapper - { - private: - IDatabaseListener* listener_; - SQLite::Connection db_; - Internals::SignalRemainingAncestor* signalRemainingAncestor_; - unsigned int version_; - - void GetChangesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults); - - void GetExportedResourcesInternal(std::list& target, - bool& done, - SQLite::Statement& s, - uint32_t maxResults); - - void ClearTable(const std::string& tableName); - - public: - DatabaseWrapper(const std::string& path); - - DatabaseWrapper(); - - virtual void Open(); - - virtual void Close() - { - db_.Close(); - } - - virtual void SetListener(IDatabaseListener& listener); - - virtual bool LookupParent(int64_t& parentId, - int64_t resourceId); - - virtual std::string GetPublicId(int64_t resourceId); - - virtual ResourceType GetResourceType(int64_t resourceId); - - virtual void DeleteResource(int64_t id); - - virtual void GetChanges(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults); - - virtual void GetLastChange(std::list& target /*out*/); - - virtual SQLite::ITransaction* StartTransaction() - { - return new SQLite::Transaction(db_); - } - - virtual void FlushToDisk() - { - db_.FlushToDisk(); - } - - virtual bool HasFlushToDisk() const - { - return true; - } - - virtual void ClearChanges() - { - ClearTable("Changes"); - } - - virtual void ClearExportedResources() - { - ClearTable("ExportedResources"); - } - - virtual void GetAllMetadata(std::map& target, - int64_t id); - - virtual unsigned int GetDatabaseVersion() - { - return version_; - } - - virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea); - - - /** - * The methods declared below are for unit testing only! - **/ - - const char* GetErrorMessage() const - { - return db_.GetErrorMessage(); - } - - void GetChildren(std::list& childrenPublicIds, - int64_t id); - - int64_t GetTableRecordCount(const std::string& table); - - bool GetParentPublicId(std::string& target, - int64_t id); - - - - /** - * Until Orthanc 1.4.0, the methods below were part of the - * "DatabaseWrapperBase" class, that is now placed in the - * graveyard. - **/ - - virtual void SetGlobalProperty(GlobalProperty property, - const std::string& value); - - virtual bool LookupGlobalProperty(std::string& target, - GlobalProperty property); - - virtual int64_t CreateResource(const std::string& publicId, - ResourceType type); - - virtual bool LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId); - - virtual void AttachChild(int64_t parent, - int64_t child); - - virtual void SetMetadata(int64_t id, - MetadataType type, - const std::string& value); - - virtual void DeleteMetadata(int64_t id, - MetadataType type); - - virtual bool LookupMetadata(std::string& target, - int64_t id, - MetadataType type); - - virtual void ListAvailableMetadata(std::list& target, - int64_t id); - - virtual void AddAttachment(int64_t id, - const FileInfo& attachment); - - virtual void DeleteAttachment(int64_t id, - FileContentType attachment); - - virtual void ListAvailableAttachments(std::list& target, - int64_t id); - - virtual bool LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType); - - virtual void ClearMainDicomTags(int64_t id); - - virtual void SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value); - - virtual void SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value); - - virtual void GetMainDicomTags(DicomMap& map, - int64_t id); - - virtual void GetChildrenPublicId(std::list& target, - int64_t id); - - virtual void GetChildrenInternalId(std::list& target, - int64_t id); - - virtual void LogChange(int64_t internalId, - const ServerIndexChange& change); - - virtual void LogExportedResource(const ExportedResource& resource); - - virtual void GetExportedResources(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults); - - virtual void GetLastExportedResource(std::list& target /*out*/); - - virtual uint64_t GetTotalCompressedSize(); - - virtual uint64_t GetTotalUncompressedSize(); - - virtual uint64_t GetResourceCount(ResourceType resourceType); - - virtual void GetAllInternalIds(std::list& target, - ResourceType resourceType); - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType); - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit); - - virtual bool SelectPatientToRecycle(int64_t& internalId); - - virtual bool SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid); - - virtual bool IsProtectedPatient(int64_t internalId); - - virtual void SetProtectedPatient(int64_t internalId, - bool isProtected); - - virtual bool IsExistingResource(int64_t internalId); - - virtual void LookupIdentifier(std::list& result, - ResourceType level, - const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value); - - virtual void LookupIdentifierRange(std::list& result, - ResourceType level, - const DicomTag& tag, - const std::string& start, - const std::string& end); - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/DicomInstanceOrigin.cpp --- a/OrthancServer/DicomInstanceOrigin.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/DicomInstanceOrigin.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -37,7 +37,6 @@ #include "../Core/OrthancException.h" #include "../Core/SerializationToolbox.h" - namespace Orthanc { void DicomInstanceOrigin::Format(Json::Value& result) const diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/IDatabaseListener.h --- a/OrthancServer/IDatabaseListener.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 -#include "ServerEnumerations.h" -#include "ServerIndexChange.h" - -namespace Orthanc -{ - class IDatabaseListener - { - public: - virtual ~IDatabaseListener() - { - } - - virtual void SignalRemainingAncestor(ResourceType parentType, - const std::string& publicId) = 0; - - virtual void SignalFileDeleted(const FileInfo& info) = 0; - - virtual void SignalChange(const ServerIndexChange& change) = 0; - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/IDatabaseWrapper.h --- a/OrthancServer/IDatabaseWrapper.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,210 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "../Core/DicomFormat/DicomMap.h" -#include "../Core/SQLite/ITransaction.h" -#include "../Core/FileStorage/IStorageArea.h" -#include "../Core/FileStorage/FileInfo.h" -#include "IDatabaseListener.h" -#include "ExportedResource.h" - -#include -#include - -namespace Orthanc -{ - class IDatabaseWrapper : public boost::noncopyable - { - public: - virtual ~IDatabaseWrapper() - { - } - - virtual void Open() = 0; - - virtual void Close() = 0; - - virtual void AddAttachment(int64_t id, - const FileInfo& attachment) = 0; - - virtual void AttachChild(int64_t parent, - int64_t child) = 0; - - virtual void ClearChanges() = 0; - - virtual void ClearExportedResources() = 0; - - virtual int64_t CreateResource(const std::string& publicId, - ResourceType type) = 0; - - virtual void DeleteAttachment(int64_t id, - FileContentType attachment) = 0; - - virtual void DeleteMetadata(int64_t id, - MetadataType type) = 0; - - virtual void DeleteResource(int64_t id) = 0; - - virtual void FlushToDisk() = 0; - - virtual bool HasFlushToDisk() const = 0; - - virtual void GetAllMetadata(std::map& target, - int64_t id) = 0; - - virtual void GetAllInternalIds(std::list& target, - ResourceType resourceType) = 0; - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType) = 0; - - virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType, - size_t since, - size_t limit) = 0; - - virtual void GetChanges(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) = 0; - - virtual void GetChildrenInternalId(std::list& target, - int64_t id) = 0; - - virtual void GetChildrenPublicId(std::list& target, - int64_t id) = 0; - - virtual void GetExportedResources(std::list& target /*out*/, - bool& done /*out*/, - int64_t since, - uint32_t maxResults) = 0; - - virtual void GetLastChange(std::list& target /*out*/) = 0; - - virtual void GetLastExportedResource(std::list& target /*out*/) = 0; - - virtual void GetMainDicomTags(DicomMap& map, - int64_t id) = 0; - - virtual std::string GetPublicId(int64_t resourceId) = 0; - - virtual uint64_t GetResourceCount(ResourceType resourceType) = 0; - - virtual ResourceType GetResourceType(int64_t resourceId) = 0; - - virtual uint64_t GetTotalCompressedSize() = 0; - - virtual uint64_t GetTotalUncompressedSize() = 0; - - virtual bool IsExistingResource(int64_t internalId) = 0; - - virtual bool IsProtectedPatient(int64_t internalId) = 0; - - virtual void ListAvailableMetadata(std::list& target, - int64_t id) = 0; - - virtual void ListAvailableAttachments(std::list& target, - int64_t id) = 0; - - virtual void LogChange(int64_t internalId, - const ServerIndexChange& change) = 0; - - virtual void LogExportedResource(const ExportedResource& resource) = 0; - - virtual bool LookupAttachment(FileInfo& attachment, - int64_t id, - FileContentType contentType) = 0; - - virtual bool LookupGlobalProperty(std::string& target, - GlobalProperty property) = 0; - - virtual void LookupIdentifier(std::list& result, - ResourceType level, - const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value) = 0; - - virtual void LookupIdentifierRange(std::list& result, - ResourceType level, - const DicomTag& tag, - const std::string& start, - const std::string& end) = 0; - - virtual bool LookupMetadata(std::string& target, - int64_t id, - MetadataType type) = 0; - - virtual bool LookupParent(int64_t& parentId, - int64_t resourceId) = 0; - - virtual bool LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId) = 0; - - virtual bool SelectPatientToRecycle(int64_t& internalId) = 0; - - virtual bool SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid) = 0; - - virtual void SetGlobalProperty(GlobalProperty property, - const std::string& value) = 0; - - virtual void ClearMainDicomTags(int64_t id) = 0; - - virtual void SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value) = 0; - - virtual void SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value) = 0; - - virtual void SetMetadata(int64_t id, - MetadataType type, - const std::string& value) = 0; - - virtual void SetProtectedPatient(int64_t internalId, - bool isProtected) = 0; - - virtual SQLite::ITransaction* StartTransaction() = 0; - - virtual void SetListener(IDatabaseListener& listener) = 0; - - virtual unsigned int GetDatabaseVersion() = 0; - - virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea) = 0; - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancConfiguration.cpp --- a/OrthancServer/OrthancConfiguration.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancConfiguration.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -34,7 +34,7 @@ #include "PrecompiledHeadersServer.h" #include "OrthancConfiguration.h" -#include "../Core/HttpServer/MongooseServer.h" +#include "../Core/HttpServer/HttpServer.h" #include "../Core/Logging.h" #include "../Core/OrthancException.h" #include "../Core/SystemToolbox.h" @@ -609,7 +609,7 @@ } - void OrthancConfiguration::SetupRegisteredUsers(MongooseServer& httpServer) const + void OrthancConfiguration::SetupRegisteredUsers(HttpServer& httpServer) const { httpServer.ClearUsers(); diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancConfiguration.h --- a/OrthancServer/OrthancConfiguration.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancConfiguration.h Thu Jan 24 10:55:19 2019 +0100 @@ -45,7 +45,7 @@ namespace Orthanc { - class MongooseServer; + class HttpServer; class ServerIndex; class OrthancConfiguration : public boost::noncopyable @@ -184,7 +184,7 @@ void GetListOfOrthancPeers(std::set& target) const; - void SetupRegisteredUsers(MongooseServer& httpServer) const; + void SetupRegisteredUsers(HttpServer& httpServer) const; std::string InterpretStringParameterAsPath(const std::string& parameter) const; diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancFindRequestHandler.cpp --- a/OrthancServer/OrthancFindRequestHandler.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -35,11 +35,12 @@ #include "OrthancFindRequestHandler.h" #include "../Core/DicomFormat/DicomArray.h" -#include "../Core/Lua/LuaFunctionCall.h" +#include "../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/Logging.h" -#include "../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Core/Lua/LuaFunctionCall.h" #include "OrthancConfiguration.h" -#include "Search/LookupResource.h" +#include "Search/DatabaseLookup.h" +#include "ServerContext.h" #include "ServerToolbox.h" #include @@ -614,7 +615,7 @@ * Build up the query object. **/ - LookupResource lookup(level); + DatabaseLookup lookup; bool caseSensitivePN; @@ -654,7 +655,7 @@ sensitive = caseSensitivePN; } - lookup.AddDicomConstraint(tag, value, sensitive); + lookup.AddDicomConstraint(tag, value, sensitive, true /* mandatory */); } else { @@ -672,7 +673,7 @@ LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn); - context_.Apply(visitor, lookup, 0 /* "since" is not relevant to C-FIND */, limit); + context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit); } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancFindRequestHandler.h --- a/OrthancServer/OrthancFindRequestHandler.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.h Thu Jan 24 10:55:19 2019 +0100 @@ -34,10 +34,10 @@ #include "../Core/DicomNetworking/IFindRequestHandler.h" -#include "ServerContext.h" - namespace Orthanc { + class ServerContext; + class OrthancFindRequestHandler : public IFindRequestHandler { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancInitialization.cpp --- a/OrthancServer/OrthancInitialization.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancInitialization.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -45,7 +45,7 @@ #include "../Core/Logging.h" #include "../Core/OrthancException.h" -#include "DatabaseWrapper.h" +#include "Database/SQLiteDatabaseWrapper.h" #include "OrthancConfiguration.h" #include // For dcmDisableGethostbyaddr() @@ -308,7 +308,7 @@ { } - return new DatabaseWrapper(indexDirectory.string() + "/index"); + return new SQLiteDatabaseWrapper(indexDirectory.string() + "/index"); } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancInitialization.h --- a/OrthancServer/OrthancInitialization.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancInitialization.h Thu Jan 24 10:55:19 2019 +0100 @@ -34,7 +34,7 @@ #pragma once #include "../Core/FileStorage/IStorageArea.h" -#include "IDatabaseWrapper.h" +#include "Database/IDatabaseWrapper.h" namespace Orthanc { diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancMoveRequestHandler.cpp --- a/OrthancServer/OrthancMoveRequestHandler.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -34,12 +34,14 @@ #include "PrecompiledHeadersServer.h" #include "OrthancMoveRequestHandler.h" -#include "OrthancConfiguration.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/DicomFormat/DicomArray.h" #include "../Core/Logging.h" +#include "OrthancConfiguration.h" +#include "ServerContext.h" #include "ServerJobs/DicomModalityStoreJob.h" + namespace Orthanc { namespace @@ -123,6 +125,7 @@ ServerContext& context_; std::auto_ptr job_; size_t position_; + size_t countInstances_; public: AsynchronousMove(ServerContext& context, @@ -154,6 +157,8 @@ std::list tmp; context_.GetIndex().GetChildInstances(tmp, publicId); + countInstances_ = tmp.size(); + job_->Reserve(tmp.size()); for (std::list::iterator it = tmp.begin(); it != tmp.end(); ++it) @@ -164,20 +169,23 @@ virtual unsigned int GetSubOperationCount() const { - return 1; + return countInstances_; } virtual Status DoNext() { + if (position_ >= countInstances_) + { + return Status_Failure; + } + if (position_ == 0) { context_.GetJobsEngine().GetRegistry().Submit(job_.release(), 0 /* priority */); - return Status_Success; } - else - { - return Status_Failure; - } + + position_ ++; + return Status_Success; } }; } @@ -226,7 +234,7 @@ const std::string& content = value.GetContent(); - std::list ids; + std::vector ids; context_.GetIndex().LookupIdentifierExact(ids, level, tag, content); if (ids.size() != 1) @@ -235,7 +243,7 @@ } else { - publicId = ids.front(); + publicId = ids[0]; return true; } } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancMoveRequestHandler.h --- a/OrthancServer/OrthancMoveRequestHandler.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancMoveRequestHandler.h Thu Jan 24 10:55:19 2019 +0100 @@ -33,10 +33,11 @@ #pragma once #include "../Core/DicomNetworking/IMoveRequestHandler.h" -#include "ServerContext.h" namespace Orthanc { + class ServerContext; + class OrthancMoveRequestHandler : public IMoveRequestHandler { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancRestApi/OrthancRestArchive.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -35,9 +35,12 @@ #include "OrthancRestApi.h" #include "../../Core/HttpServer/FilesystemHttpSender.h" +#include "../../Core/OrthancException.h" #include "../../Core/SerializationToolbox.h" +#include "../ServerContext.h" #include "../ServerJobs/ArchiveJob.h" + namespace Orthanc { static const char* const KEY_RESOURCES = "Resources"; diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -34,11 +34,14 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" +#include "../../Core/Cache/SharedArchive.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" + #include "../OrthancConfiguration.h" #include "../QueryRetrieveHandler.h" +#include "../ServerContext.h" #include "../ServerJobs/DicomModalityStoreJob.h" #include "../ServerJobs/DicomMoveScuJob.h" #include "../ServerJobs/OrthancPeerStoreJob.h" diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -41,11 +41,13 @@ #include "../../Core/Logging.h" #include "../DefaultDicomImageDecoder.h" #include "../OrthancConfiguration.h" -#include "../Search/LookupResource.h" +#include "../Search/DatabaseLookup.h" #include "../ServerContext.h" #include "../ServerToolbox.h" #include "../SliceOrdering.h" +#include "../../Plugins/Engine/OrthancPlugins.h" + namespace Orthanc { @@ -1228,13 +1230,12 @@ const std::string& value, ResourceType level) { - std::list tmp; + std::vector tmp; index.LookupIdentifierExact(tmp, level, tag, value); - for (std::list::const_iterator - it = tmp.begin(); it != tmp.end(); ++it) + for (size_t i = 0; i < tmp.size(); i++) { - result.push_back(std::make_pair(level, *it)); + result.push_back(std::make_pair(level, tmp[i])); } } @@ -1401,9 +1402,9 @@ since = static_cast(tmp); } - std::string level = request[KEY_LEVEL].asString(); + ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); - LookupResource query(StringToResourceType(level.c_str())); + DatabaseLookup query; Json::Value::Members members = request[KEY_QUERY].getMemberNames(); for (size_t i = 0; i < members.size(); i++) @@ -1414,14 +1415,14 @@ "Tag \"" + members[i] + "\" should be associated with a string"); } - query.AddDicomConstraint(FromDcmtkBridge::ParseTag(members[i]), - request[KEY_QUERY][members[i]].asString(), - caseSensitive); + query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), + request[KEY_QUERY][members[i]].asString(), + caseSensitive, true); } FindVisitor visitor; - context.Apply(visitor, query, since, limit); - visitor.Answer(call.GetOutput(), context.GetIndex(), query.GetLevel(), expand); + context.Apply(visitor, query, level, since, limit); + visitor.Answer(call.GetOutput(), context.GetIndex(), level, expand); } } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/OrthancRestApi/OrthancRestSystem.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -34,10 +34,10 @@ #include "../PrecompiledHeadersServer.h" #include "OrthancRestApi.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" +#include "../../Plugins/Engine/OrthancPlugins.h" +#include "../../Plugins/Engine/PluginsManager.h" #include "../OrthancConfiguration.h" -#include "../../Core/DicomParsing/FromDcmtkBridge.h" -#include "../../Plugins/Engine/PluginsManager.h" -#include "../../Plugins/Engine/OrthancPlugins.h" #include "../ServerContext.h" diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/PrecompiledHeadersServer.h --- a/OrthancServer/PrecompiledHeadersServer.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/PrecompiledHeadersServer.h Thu Jan 24 10:55:19 2019 +0100 @@ -37,6 +37,6 @@ #if ORTHANC_USE_PRECOMPILED_HEADERS == 1 -#include "ServerContext.h" +#include #endif diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/PrepareDatabase.sql --- a/OrthancServer/PrepareDatabase.sql Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,126 +0,0 @@ -CREATE TABLE GlobalProperties( - property INTEGER PRIMARY KEY, - value TEXT - ); - -CREATE TABLE Resources( - internalId INTEGER PRIMARY KEY AUTOINCREMENT, - resourceType INTEGER, - publicId TEXT, - parentId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE - ); - -CREATE TABLE MainDicomTags( - id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, - tagGroup INTEGER, - tagElement INTEGER, - value TEXT, - PRIMARY KEY(id, tagGroup, tagElement) - ); - --- The following table was added in Orthanc 0.8.5 (database v5) -CREATE TABLE DicomIdentifiers( - id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, - tagGroup INTEGER, - tagElement INTEGER, - value TEXT, - PRIMARY KEY(id, tagGroup, tagElement) - ); - -CREATE TABLE Metadata( - id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, - type INTEGER, - value TEXT, - PRIMARY KEY(id, type) - ); - -CREATE TABLE AttachedFiles( - id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, - fileType INTEGER, - uuid TEXT, - compressedSize INTEGER, - uncompressedSize INTEGER, - compressionType INTEGER, - uncompressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) - compressedMD5 TEXT, -- New in Orthanc 0.7.3 (database v4) - PRIMARY KEY(id, fileType) - ); - -CREATE TABLE Changes( - seq INTEGER PRIMARY KEY AUTOINCREMENT, - changeType INTEGER, - internalId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, - resourceType INTEGER, - date TEXT - ); - -CREATE TABLE ExportedResources( - seq INTEGER PRIMARY KEY AUTOINCREMENT, - resourceType INTEGER, - publicId TEXT, - remoteModality TEXT, - patientId TEXT, - studyInstanceUid TEXT, - seriesInstanceUid TEXT, - sopInstanceUid TEXT, - date TEXT - ); - -CREATE TABLE PatientRecyclingOrder( - seq INTEGER PRIMARY KEY AUTOINCREMENT, - patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE - ); - -CREATE INDEX ChildrenIndex ON Resources(parentId); -CREATE INDEX PublicIndex ON Resources(publicId); -CREATE INDEX ResourceTypeIndex ON Resources(resourceType); -CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId); - -CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id); --- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up --- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement); --- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY); - --- The 3 following indexes were added in Orthanc 0.8.5 (database v5) -CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); -CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement); -CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY); - -CREATE INDEX ChangesIndex ON Changes(internalId); - -CREATE TRIGGER AttachedFileDeleted -AFTER DELETE ON AttachedFiles -BEGIN - SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, - old.compressionType, old.compressedSize, - -- These 2 arguments are new in Orthanc 0.7.3 (database v4) - old.uncompressedMD5, old.compressedMD5); -END; - -CREATE TRIGGER ResourceDeleted -AFTER DELETE ON Resources -BEGIN - SELECT SignalResourceDeleted(old.publicId, old.resourceType); -- New in Orthanc 0.8.5 (db v5) - SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) - FROM Resources AS parent WHERE internalId = old.parentId; -END; - --- Delete a parent resource when its unique child is deleted -CREATE TRIGGER ResourceDeletedParentCleaning -AFTER DELETE ON Resources -FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0 -BEGIN - DELETE FROM Resources WHERE internalId = old.parentId; -END; - -CREATE TRIGGER PatientAdded -AFTER INSERT ON Resources -FOR EACH ROW WHEN new.resourceType = 1 -- "1" corresponds to "ResourceType_Patient" in C++ -BEGIN - INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId); -END; - - --- Set the version of the database schema --- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration -INSERT INTO GlobalProperties VALUES (1, "6"); diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/QueryRetrieveHandler.cpp --- a/OrthancServer/QueryRetrieveHandler.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/QueryRetrieveHandler.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -38,6 +38,8 @@ #include "../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/Logging.h" +#include "LuaScripting.h" +#include "ServerContext.h" namespace Orthanc diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/QueryRetrieveHandler.h --- a/OrthancServer/QueryRetrieveHandler.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/QueryRetrieveHandler.h Thu Jan 24 10:55:19 2019 +0100 @@ -33,10 +33,13 @@ #pragma once -#include "ServerContext.h" +#include "../Core/DicomNetworking/DicomFindAnswers.h" +#include "../Core/DicomNetworking/RemoteModalityParameters.h" namespace Orthanc { + class ServerContext; + class QueryRetrieveHandler : public IDynamicObject { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/DatabaseConstraint.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DatabaseConstraint.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,245 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "DatabaseConstraint.h" + +#include "../../Core/OrthancException.h" + + +namespace Orthanc +{ + namespace Plugins + { +#if ORTHANC_ENABLE_PLUGINS == 1 + OrthancPluginResourceType Convert(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return OrthancPluginResourceType_Patient; + + case ResourceType_Study: + return OrthancPluginResourceType_Study; + + case ResourceType_Series: + return OrthancPluginResourceType_Series; + + case ResourceType_Instance: + return OrthancPluginResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +#endif + + +#if ORTHANC_ENABLE_PLUGINS == 1 + ResourceType Convert(OrthancPluginResourceType type) + { + switch (type) + { + case OrthancPluginResourceType_Patient: + return ResourceType_Patient; + + case OrthancPluginResourceType_Study: + return ResourceType_Study; + + case OrthancPluginResourceType_Series: + return ResourceType_Series; + + case OrthancPluginResourceType_Instance: + return ResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + OrthancPluginConstraintType Convert(ConstraintType constraint) + { + switch (constraint) + { + case ConstraintType_Equal: + return OrthancPluginConstraintType_Equal; + + case ConstraintType_GreaterOrEqual: + return OrthancPluginConstraintType_GreaterOrEqual; + + case ConstraintType_SmallerOrEqual: + return OrthancPluginConstraintType_SmallerOrEqual; + + case ConstraintType_Wildcard: + return OrthancPluginConstraintType_Wildcard; + + case ConstraintType_List: + return OrthancPluginConstraintType_List; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +#endif + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + ConstraintType Convert(OrthancPluginConstraintType constraint) + { + switch (constraint) + { + case OrthancPluginConstraintType_Equal: + return ConstraintType_Equal; + + case OrthancPluginConstraintType_GreaterOrEqual: + return ConstraintType_GreaterOrEqual; + + case OrthancPluginConstraintType_SmallerOrEqual: + return ConstraintType_SmallerOrEqual; + + case OrthancPluginConstraintType_Wildcard: + return ConstraintType_Wildcard; + + case OrthancPluginConstraintType_List: + return ConstraintType_List; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +#endif + } + + DatabaseConstraint::DatabaseConstraint(ResourceType level, + const DicomTag& tag, + bool isIdentifier, + ConstraintType type, + const std::vector& values, + bool caseSensitive, + bool mandatory) : + level_(level), + tag_(tag), + isIdentifier_(isIdentifier), + constraintType_(type), + values_(values), + caseSensitive_(caseSensitive), + mandatory_(mandatory) + { + if (type != ConstraintType_List && + values_.size() != 1) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + DatabaseConstraint::DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint) : + level_(Plugins::Convert(constraint.level)), + tag_(constraint.tagGroup, constraint.tagElement), + isIdentifier_(constraint.isIdentifierTag), + constraintType_(Plugins::Convert(constraint.type)), + caseSensitive_(constraint.isCaseSensitive), + mandatory_(constraint.isMandatory) + { + if (constraintType_ != ConstraintType_List && + constraint.valuesCount != 1) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + values_.resize(constraint.valuesCount); + + for (uint32_t i = 0; i < constraint.valuesCount; i++) + { + assert(constraint.values[i] != NULL); + values_[i].assign(constraint.values[i]); + } + } +#endif + + + const std::string& DatabaseConstraint::GetValue(size_t index) const + { + if (index >= values_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return values_[index]; + } + } + + + const std::string& DatabaseConstraint::GetSingleValue() const + { + if (values_.size() != 1) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return values_[0]; + } + } + + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + void DatabaseConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint, + std::vector& tmpValues) const + { + memset(&constraint, 0, sizeof(constraint)); + + tmpValues.resize(values_.size()); + + for (size_t i = 0; i < values_.size(); i++) + { + tmpValues[i] = values_[i].c_str(); + } + + constraint.level = Plugins::Convert(level_); + constraint.tagGroup = tag_.GetGroup(); + constraint.tagElement = tag_.GetElement(); + constraint.isIdentifierTag = isIdentifier_; + constraint.isCaseSensitive = caseSensitive_; + constraint.isMandatory = mandatory_; + constraint.type = Plugins::Convert(constraintType_); + constraint.valuesCount = values_.size(); + constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]); + } +#endif +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/DatabaseConstraint.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/DatabaseConstraint.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,144 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../../Core/DicomFormat/DicomMap.h" +#include "../ServerEnumerations.h" + +#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0 + +#if ORTHANC_ENABLE_PLUGINS == 1 +# include +# if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) // Macro introduced in 1.3.1 +# if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 2) +# undef ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT +# define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 1 +# endif +# endif +#endif + +namespace Orthanc +{ + namespace Plugins + { +#if ORTHANC_ENABLE_PLUGINS == 1 + OrthancPluginResourceType Convert(ResourceType type); +#endif + +#if ORTHANC_ENABLE_PLUGINS == 1 + ResourceType Convert(OrthancPluginResourceType type); +#endif + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + OrthancPluginConstraintType Convert(ConstraintType constraint); +#endif + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + ConstraintType Convert(OrthancPluginConstraintType constraint); +#endif + } + + + // This class is also used by the "orthanc-databases" project + class DatabaseConstraint + { + private: + ResourceType level_; + DicomTag tag_; + bool isIdentifier_; + ConstraintType constraintType_; + std::vector values_; + bool caseSensitive_; + bool mandatory_; + + public: + DatabaseConstraint(ResourceType level, + const DicomTag& tag, + bool isIdentifier, + ConstraintType type, + const std::vector& values, + bool caseSensitive, + bool mandatory); + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint); +#endif + + ResourceType GetLevel() const + { + return level_; + } + + const DicomTag& GetTag() const + { + return tag_; + } + + bool IsIdentifier() const + { + return isIdentifier_; + } + + ConstraintType GetConstraintType() const + { + return constraintType_; + } + + size_t GetValuesCount() const + { + return values_.size(); + } + + const std::string& GetValue(size_t index) const; + + const std::string& GetSingleValue() const; + + bool IsCaseSensitive() const + { + return caseSensitive_; + } + + bool IsMandatory() const + { + return mandatory_; + } + + bool IsMatch(const DicomMap& dicom) const; + +#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1 + void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint, + std::vector& tmpValues) const; +#endif + }; +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/DatabaseLookup.cpp --- a/OrthancServer/Search/DatabaseLookup.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/Search/DatabaseLookup.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -36,53 +36,12 @@ #include "../ServerToolbox.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" +#include "../../Core/DicomParsing/ToDcmtkBridge.h" +#include "../../Core/OrthancException.h" +#include "../../Core/Toolbox.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++) @@ -116,22 +75,11 @@ 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) + bool DatabaseLookup::IsMatch(const DicomMap& value) const { for (size_t i = 0; i < constraints_.size(); i++) { @@ -146,9 +94,126 @@ } + bool DatabaseLookup::IsMatch(DcmItem& item, + Encoding encoding) const + { + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + + const bool isOptionalConstraint = !constraints_[i]->IsMandatory(); + const DcmTagKey tag = ToDcmtkBridge::Convert(constraints_[i]->GetTag()); + + DcmElement* element = NULL; + if (!item.findAndGetElement(tag, element).good()) + { + return isOptionalConstraint; + } + + if (element == NULL) + { + return false; + } + + std::set ignoreTagLength; + std::auto_ptr value(FromDcmtkBridge::ConvertLeafElement + (*element, DicomToJsonFlags_None, + 0, encoding, ignoreTagLength)); + + // WARNING: Also modify "HierarchicalMatcher::Setup()" if modifying this code + if (value.get() == NULL || + value->IsNull()) + { + return isOptionalConstraint; + } + else if (value->IsBinary() || + !constraints_[i]->IsMatch(value->GetContent())) + { + return false; + } + } + + return true; + } + + + void DatabaseLookup::AddDicomConstraintInternal(const DicomTag& tag, + ValueRepresentation vr, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag) + { + 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, mandatoryTag)); + } + + if (!upper.empty()) + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive, mandatoryTag)); + } + } + 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, mandatoryTag)); + + 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, mandatoryTag)); + } + else + { + AddConstraint(new DicomTagConstraint + (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag)); + } + } + + void DatabaseLookup::AddDicomConstraint(const DicomTag& tag, const std::string& dicomQuery, - bool caseSensitivePN) + bool caseSensitivePN, + bool mandatoryTag) { ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); @@ -188,75 +253,44 @@ * (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)); + AddDicomConstraintInternal(tag, vr, dicomQuery, caseSensitivePN, mandatoryTag); } else { - AddConstraint(new DicomTagConstraint - (tag, ConstraintType_Equal, dicomQuery, caseSensitive)); + 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); + } + + + bool DatabaseLookup::HasOnlyMainDicomTags() const + { + std::set mainTags; + DicomMap::GetMainDicomTags(mainTags); + + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + + if (mainTags.find(constraints_[i]->GetTag()) == mainTags.end()) + { + // This is not a main DICOM tag + return false; + } + } + + return true; + } } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/DatabaseLookup.h --- a/OrthancServer/Search/DatabaseLookup.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/Search/DatabaseLookup.h Thu Jan 24 10:55:19 2019 +0100 @@ -35,49 +35,24 @@ #include "DicomTagConstraint.h" +class DcmItem; + namespace Orthanc { class DatabaseLookup : public boost::noncopyable { private: - class TagInfo - { - private: - DicomTagType type_; - ResourceType level_; - - public: - TagInfo() : - type_(DicomTagType_Generic), - level_(ResourceType_Instance) - { - } + std::vector constraints_; - 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); - + void AddDicomConstraintInternal(const DicomTag& tag, + ValueRepresentation vr, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag); public: - DatabaseLookup(); + DatabaseLookup() + { + } ~DatabaseLookup(); @@ -95,10 +70,21 @@ void AddConstraint(DicomTagConstraint* constraint); // Takes ownership - bool IsMatch(const DicomMap& value); + bool IsMatch(const DicomMap& value) const; + + bool IsMatch(DcmItem& item, + Encoding encoding) const; void AddDicomConstraint(const DicomTag& tag, const std::string& dicomQuery, - bool caseSensitivePN); + bool caseSensitivePN, + bool mandatoryTag); + + void AddRestConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive, + bool mandatoryTag); + + bool HasOnlyMainDicomTags() const; }; } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/DicomTagConstraint.cpp --- a/OrthancServer/Search/DicomTagConstraint.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/Search/DicomTagConstraint.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -34,8 +34,13 @@ #include "../PrecompiledHeadersServer.h" #include "DicomTagConstraint.h" +#if defined(ORTHANC_ENABLE_LUA) && ORTHANC_ENABLE_LUA != 0 +# include "../ServerToolbox.h" +#endif + #include "../../Core/OrthancException.h" #include "../../Core/Toolbox.h" +#include "DatabaseConstraint.h" #include @@ -94,32 +99,24 @@ }; - 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) + void DicomTagConstraint::AssignSingleValue(const std::string& value) { - if (type == ConstraintType_Equal || - type == ConstraintType_SmallerOrEqual || - type == ConstraintType_GreaterOrEqual || - type == ConstraintType_Wildcard) - { - values_.insert(value); - } - else + if (constraintType_ != ConstraintType_Wildcard && + (value.find('*') != std::string::npos || + value.find('?') != std::string::npos)) { throw OrthancException(ErrorCode_ParameterOutOfRange); } - if (type != ConstraintType_Wildcard && - (value.find('*') != std::string::npos || - value.find('?') != std::string::npos)) + if (constraintType_ == ConstraintType_Equal || + constraintType_ == ConstraintType_SmallerOrEqual || + constraintType_ == ConstraintType_GreaterOrEqual || + constraintType_ == ConstraintType_Wildcard) + { + values_.clear(); + values_.insert(value); + } + else { throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -128,13 +125,26 @@ DicomTagConstraint::DicomTagConstraint(const DicomTag& tag, ConstraintType type, - bool caseSensitive) : - hasTagInfo_(false), - tagType_(DicomTagType_Generic), // Dummy initialization - level_(ResourceType_Patient), // Dummy initialization + const std::string& value, + bool caseSensitive, + bool mandatory) : tag_(tag), constraintType_(type), - caseSensitive_(caseSensitive) + caseSensitive_(caseSensitive), + mandatory_(mandatory) + { + AssignSingleValue(value); + } + + + DicomTagConstraint::DicomTagConstraint(const DicomTag& tag, + ConstraintType type, + bool caseSensitive, + bool mandatory) : + tag_(tag), + constraintType_(type), + caseSensitive_(caseSensitive), + mandatory_(mandatory) { if (type != ConstraintType_List) { @@ -143,37 +153,33 @@ } - void DicomTagConstraint::SetTagInfo(DicomTagType tagType, - ResourceType level) + DicomTagConstraint::DicomTagConstraint(const DatabaseConstraint& constraint) : + tag_(constraint.GetTag()), + constraintType_(constraint.GetConstraintType()), + caseSensitive_(constraint.IsCaseSensitive()), + mandatory_(constraint.IsMandatory()) { - hasTagInfo_ = true; - tagType_ = tagType; - level_ = level; - } - - - DicomTagType DicomTagConstraint::GetTagType() const - { - if (!hasTagInfo_) +#if defined(ORTHANC_ENABLE_LUA) && ORTHANC_ENABLE_LUA != 0 + assert(constraint.IsIdentifier() == + ServerToolbox::IsIdentifier(constraint.GetTag(), constraint.GetLevel())); +#endif + + if (constraint.IsIdentifier()) { + // This conversion is only available for main DICOM tags, not for identifers throw OrthancException(ErrorCode_BadSequenceOfCalls); } + + if (constraintType_ == ConstraintType_List) + { + for (size_t i = 0; i < constraint.GetValuesCount(); i++) + { + AddValue(constraint.GetValue(i)); + } + } else { - return tagType_; - } - } - - - const ResourceType DicomTagConstraint::GetLevel() const - { - if (!hasTagInfo_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - return level_; + AssignSingleValue(constraint.GetSingleValue()); } } @@ -268,8 +274,18 @@ const DicomValue* tmp = value.TestAndGetValue(tag_); if (tmp == NULL || - tmp->IsNull() || - tmp->IsBinary()) + tmp->IsNull()) + { + if (mandatory_) + { + return false; + } + else + { + return true; + } + } + else if (tmp->IsBinary()) { return false; } @@ -278,4 +294,91 @@ return IsMatch(tmp->GetContent()); } } + + + std::string DicomTagConstraint::Format() const + { + switch (constraintType_) + { + case ConstraintType_Equal: + return tag_.Format() + " == " + GetValue(); + + case ConstraintType_SmallerOrEqual: + return tag_.Format() + " <= " + GetValue(); + + case ConstraintType_GreaterOrEqual: + return tag_.Format() + " >= " + GetValue(); + + case ConstraintType_Wildcard: + return tag_.Format() + " ~~ " + GetValue(); + + case ConstraintType_List: + { + std::string s = tag_.Format() + " IN [ "; + + bool first = true; + for (std::set::const_iterator + it = values_.begin(); it != values_.end(); ++it) + { + if (first) + { + first = false; + } + else + { + s += ", "; + } + + s += *it; + } + + return s + "]"; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + DatabaseConstraint DicomTagConstraint::ConvertToDatabaseConstraint(ResourceType level, + DicomTagType tagType) const + { + bool isIdentifier, caseSensitive; + + switch (tagType) + { + case DicomTagType_Identifier: + isIdentifier = true; + caseSensitive = true; + break; + + case DicomTagType_Main: + isIdentifier = false; + caseSensitive = IsCaseSensitive(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::vector values; + values.reserve(values_.size()); + + for (std::set::const_iterator + it = values_.begin(); it != values_.end(); ++it) + { + if (isIdentifier) + { + values.push_back(ServerToolbox::NormalizeIdentifier(*it)); + } + else + { + values.push_back(*it); + } + } + + return DatabaseConstraint(level, tag_, isIdentifier, constraintType_, + values, caseSensitive, mandatory_); + } } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/DicomTagConstraint.h --- a/OrthancServer/Search/DicomTagConstraint.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/Search/DicomTagConstraint.h Thu Jan 24 10:55:19 2019 +0100 @@ -35,6 +35,7 @@ #include "../ServerEnumerations.h" #include "../../Core/DicomFormat/DicomMap.h" +#include "DatabaseConstraint.h" #include @@ -46,37 +47,30 @@ class NormalizedString; class RegularExpression; - bool hasTagInfo_; - DicomTagType tagType_; - ResourceType level_; DicomTag tag_; ConstraintType constraintType_; std::set values_; bool caseSensitive_; + bool mandatory_; boost::shared_ptr regex_; + void AssignSingleValue(const std::string& value); + public: DicomTagConstraint(const DicomTag& tag, ConstraintType type, const std::string& value, - bool caseSensitive); + bool caseSensitive, + bool mandatory); + // For list search DicomTagConstraint(const DicomTag& tag, ConstraintType type, - bool caseSensitive); - - bool HasTagInfo() const - { - return hasTagInfo_; - } + bool caseSensitive, + bool mandatory); - void SetTagInfo(DicomTagType tagType, - ResourceType level); - - DicomTagType GetTagType() const; - - const ResourceType GetLevel() const; + DicomTagConstraint(const DatabaseConstraint& constraint); const DicomTag& GetTag() const { @@ -93,6 +87,16 @@ return caseSensitive_; } + void SetCaseSensitive(bool caseSensitive) + { + caseSensitive_ = caseSensitive; + } + + bool IsMandatory() const + { + return mandatory_; + } + void AddValue(const std::string& value); const std::string& GetValue() const; @@ -105,5 +109,10 @@ bool IsMatch(const std::string& value); bool IsMatch(const DicomMap& value); + + std::string Format() const; + + DatabaseConstraint ConvertToDatabaseConstraint(ResourceType level, + DicomTagType tagType) const; }; } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/HierarchicalMatcher.cpp --- a/OrthancServer/Search/HierarchicalMatcher.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/Search/HierarchicalMatcher.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -59,15 +59,6 @@ HierarchicalMatcher::~HierarchicalMatcher() { - for (Constraints::iterator it = constraints_.begin(); - it != constraints_.end(); ++it) - { - if (it->second != NULL) - { - delete it->second; - } - } - for (Sequences::iterator it = sequences_.begin(); it != sequences_.end(); ++it) { @@ -98,15 +89,14 @@ continue; } - ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); - - if (constraints_.find(tag) != constraints_.end() || + if (flatTags_.find(tag) != flatTags_.end() || sequences_.find(tag) != sequences_.end()) { + // A constraint already exists on this tag throw OrthancException(ErrorCode_BadRequest); } - if (vr == ValueRepresentation_Sequence) + if (FromDcmtkBridge::LookupValueRepresentation(tag) == ValueRepresentation_Sequence) { DcmSequenceOfItems& sequence = dynamic_cast(*element); @@ -127,12 +117,20 @@ } else { + flatTags_.insert(tag); + std::set ignoreTagLength; std::auto_ptr value(FromDcmtkBridge::ConvertLeafElement (*element, DicomToJsonFlags_None, - ORTHANC_MAXIMUM_TAG_LENGTH, encoding, ignoreTagLength)); + 0, encoding, ignoreTagLength)); - if (value->IsBinary()) + // WARNING: Also modify "DatabaseLookup::IsMatch()" if modifying this code + if (value.get() == NULL || + value->IsNull()) + { + // This is an universal constraint + } + else if (value->IsBinary()) { if (!value->GetContent().empty()) { @@ -140,26 +138,15 @@ << tag.Format() << ") with UN (unknown) value representation. " << "It will be ignored."; } - - constraints_[tag] = NULL; } - else if (value->IsNull() || - value->GetContent().empty()) + else if (value->GetContent().empty()) { // This is an universal matcher - constraints_[tag] = NULL; } else { - // DICOM specifies that searches must be case sensitive, except - // for tags with a PN value representation - bool sensitive = true; - if (vr == ValueRepresentation_PersonName) - { - sensitive = caseSensitivePN; - } - - constraints_[tag] = IFindConstraint::ParseDicomConstraint(tag, value->GetContent(), sensitive); + flatConstraints_.AddDicomConstraint + (tag, value->GetContent(), caseSensitivePN, true /* mandatory */); } } } @@ -169,19 +156,23 @@ std::string HierarchicalMatcher::Format(const std::string& prefix) const { std::string s; - - for (Constraints::const_iterator it = constraints_.begin(); - it != constraints_.end(); ++it) + + std::set tags; + for (size_t i = 0; i < flatConstraints_.GetConstraintsCount(); i++) { - s += prefix + it->first.Format() + " "; + const DicomTagConstraint& c = flatConstraints_.GetConstraint(i); - if (it->second == NULL) + s += c.Format() + "\n"; + tags.insert(c.GetTag()); + } + + // Loop over the universal constraints + for (std::set::const_iterator it = flatTags_.begin(); + it != flatTags_.end(); ++it) + { + if (tags.find(*it) == tags.end()) { - s += "*\n"; - } - else - { - s += it->second->Format() + "\n"; + s += prefix + it->Format() + " == *\n"; } } @@ -214,34 +205,11 @@ bool HierarchicalMatcher::MatchInternal(DcmItem& item, Encoding encoding) const { - for (Constraints::const_iterator it = constraints_.begin(); - it != constraints_.end(); ++it) + if (!flatConstraints_.IsMatch(item, encoding)) { - if (it->second != NULL) - { - DcmTagKey tag = ToDcmtkBridge::Convert(it->first); - - DcmElement* element = NULL; - if (!item.findAndGetElement(tag, element).good() || - element == NULL) - { - return false; - } - - std::set ignoreTagLength; - std::auto_ptr value(FromDcmtkBridge::ConvertLeafElement - (*element, DicomToJsonFlags_None, - ORTHANC_MAXIMUM_TAG_LENGTH, encoding, ignoreTagLength)); - - if (value->IsNull() || - value->IsBinary() || - !it->second->Match(value->GetContent())) - { - return false; - } - } + return false; } - + for (Sequences::const_iterator it = sequences_.begin(); it != sequences_.end(); ++it) { @@ -283,16 +251,16 @@ { std::auto_ptr target(new DcmDataset); - for (Constraints::const_iterator it = constraints_.begin(); - it != constraints_.end(); ++it) + for (std::set::const_iterator it = flatTags_.begin(); + it != flatTags_.end(); ++it) { - DcmTagKey tag = ToDcmtkBridge::Convert(it->first); + DcmTagKey tag = ToDcmtkBridge::Convert(*it); DcmElement* element = NULL; if (source.findAndGetElement(tag, element).good() && element != NULL) { - std::auto_ptr cloned(FromDcmtkBridge::CreateElementForTag(it->first)); + std::auto_ptr cloned(FromDcmtkBridge::CreateElementForTag(*it)); cloned->copyFrom(*element); target->insert(cloned.release()); } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/HierarchicalMatcher.h --- a/OrthancServer/Search/HierarchicalMatcher.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/Search/HierarchicalMatcher.h Thu Jan 24 10:55:19 2019 +0100 @@ -33,7 +33,7 @@ #pragma once -#include "IFindConstraint.h" +#include "DatabaseLookup.h" #include "../../Core/DicomParsing/ParsedDicomFile.h" class DcmItem; @@ -43,11 +43,11 @@ class HierarchicalMatcher : public boost::noncopyable { private: - typedef std::map Constraints; typedef std::map Sequences; - Constraints constraints_; - Sequences sequences_; + std::set flatTags_; + DatabaseLookup flatConstraints_; + Sequences sequences_; void Setup(DcmItem& query, bool caseSensitivePN, diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/IFindConstraint.cpp --- a/OrthancServer/Search/IFindConstraint.cpp Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,131 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "IFindConstraint.h" - -#include "ListConstraint.h" -#include "RangeConstraint.h" -#include "ValueConstraint.h" -#include "WildcardConstraint.h" - -#include "../../Core/DicomParsing/FromDcmtkBridge.h" -#include "../../Core/OrthancException.h" - -namespace Orthanc -{ - IFindConstraint* IFindConstraint::ParseDicomConstraint(const DicomTag& tag, - const std::string& dicomQuery, - bool caseSensitive) - { - ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag); - - if (vr == ValueRepresentation_Sequence) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - 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); - return new RangeConstraint(lower, upper, caseSensitive); - } - else if (dicomQuery.find('\\') != std::string::npos) - { - std::auto_ptr constraint(new ListConstraint(caseSensitive)); - - std::vector items; - Toolbox::TokenizeString(items, dicomQuery, '\\'); - - for (size_t i = 0; i < items.size(); i++) - { - constraint->AddAllowedValue(items[i]); - } - - return constraint.release(); - } - else if (dicomQuery.find('*') != std::string::npos || - dicomQuery.find('?') != std::string::npos) - { - return new WildcardConstraint(dicomQuery, caseSensitive); - } - else - { - /** - * Case-insensitive match for PN value representation (Patient - * Name). Case-senstive match for all the other value - * representations. - * - * 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 - **/ - - return new ValueConstraint(dicomQuery, caseSensitive); - } - } -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/IFindConstraint.h --- a/OrthancServer/Search/IFindConstraint.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,60 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "LookupIdentifierQuery.h" - -namespace Orthanc -{ - class IFindConstraint : public boost::noncopyable - { - public: - virtual ~IFindConstraint() - { - } - - virtual IFindConstraint* Clone() const = 0; - - virtual void Setup(LookupIdentifierQuery& lookup, - const DicomTag& tag) const = 0; - - virtual bool Match(const std::string& value) const = 0; - - virtual std::string Format() const = 0; - - static IFindConstraint* ParseDicomConstraint(const DicomTag& tag, - const std::string& dicomQuery, - bool caseSensitive); - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/ISqlLookupFormatter.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/ISqlLookupFormatter.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,343 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "ISqlLookupFormatter.h" + +#include "../../Core/OrthancException.h" +#include "DatabaseConstraint.h" + +namespace Orthanc +{ + static std::string FormatLevel(ResourceType level) + { + switch (level) + { + case ResourceType_Patient: + return "patients"; + + case ResourceType_Study: + return "studies"; + + case ResourceType_Series: + return "series"; + + case ResourceType_Instance: + return "instances"; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + static bool FormatComparison(std::string& target, + ISqlLookupFormatter& formatter, + const DatabaseConstraint& constraint, + size_t index) + { + std::string tag = "t" + boost::lexical_cast(index); + + std::string comparison; + + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + case ConstraintType_SmallerOrEqual: + case ConstraintType_GreaterOrEqual: + { + std::string op; + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + op = "="; + break; + + case ConstraintType_SmallerOrEqual: + op = "<="; + break; + + case ConstraintType_GreaterOrEqual: + op = ">="; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue()); + + if (constraint.IsCaseSensitive()) + { + comparison = tag + ".value " + op + " " + parameter; + } + else + { + comparison = "lower(" + tag + ".value) " + op + " lower(" + parameter + ")"; + } + + break; + } + + case ConstraintType_List: + { + for (size_t i = 0; i < constraint.GetValuesCount(); i++) + { + if (!comparison.empty()) + { + comparison += ", "; + } + + std::string parameter = formatter.GenerateParameter(constraint.GetValue(i)); + + if (constraint.IsCaseSensitive()) + { + comparison += parameter; + } + else + { + comparison += "lower(" + parameter + ")"; + } + } + + if (constraint.IsCaseSensitive()) + { + comparison = tag + ".value IN (" + comparison + ")"; + } + else + { + comparison = "lower(" + tag + ".value) IN (" + comparison + ")"; + } + + break; + } + + case ConstraintType_Wildcard: + { + const std::string value = constraint.GetSingleValue(); + + if (value == "*") + { + if (!constraint.IsMandatory()) + { + // Universal constraint on an optional tag, ignore it + return false; + } + } + else + { + std::string escaped; + escaped.reserve(value.size()); + + for (size_t i = 0; i < value.size(); i++) + { + if (value[i] == '*') + { + escaped += "%"; + } + else if (value[i] == '?') + { + escaped += "_"; + } + else if (value[i] == '%') + { + escaped += "\\%"; + } + else if (value[i] == '_') + { + escaped += "\\_"; + } + else if (value[i] == '\\') + { + escaped += "\\\\"; + } + else + { + escaped += value[i]; + } + } + + std::string parameter = formatter.GenerateParameter(escaped); + + if (constraint.IsCaseSensitive()) + { + comparison = (tag + ".value LIKE " + parameter + " " + + formatter.FormatWildcardEscape()); + } + else + { + comparison = ("lower(" + tag + ".value) LIKE lower(" + + parameter + ") " + formatter.FormatWildcardEscape()); + } + } + + break; + } + + default: + return false; + } + + if (constraint.IsMandatory()) + { + target = comparison; + } + else if (comparison.empty()) + { + target = tag + ".value IS NULL"; + } + else + { + target = tag + ".value IS NULL OR " + comparison; + } + + return true; + } + + + static void FormatJoin(std::string& target, + const DatabaseConstraint& constraint, + size_t index) + { + std::string tag = "t" + boost::lexical_cast(index); + + if (constraint.IsMandatory()) + { + target = " INNER JOIN "; + } + else + { + target = " LEFT JOIN "; + } + + if (constraint.IsIdentifier()) + { + target += "DicomIdentifiers "; + } + else + { + target += "MainDicomTags "; + } + + target += (tag + " ON " + tag + ".id = " + FormatLevel(constraint.GetLevel()) + + ".internalId AND " + tag + ".tagGroup = " + + boost::lexical_cast(constraint.GetTag().GetGroup()) + + " AND " + tag + ".tagElement = " + + boost::lexical_cast(constraint.GetTag().GetElement())); + } + + + void ISqlLookupFormatter::Apply(std::string& sql, + ISqlLookupFormatter& formatter, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) + { + assert(ResourceType_Patient < ResourceType_Study && + ResourceType_Study < ResourceType_Series && + ResourceType_Series < ResourceType_Instance); + + ResourceType upperLevel = queryLevel; + ResourceType lowerLevel = queryLevel; + + for (size_t i = 0; i < lookup.size(); i++) + { + ResourceType level = lookup[i].GetLevel(); + + if (level < upperLevel) + { + upperLevel = level; + } + + if (level > lowerLevel) + { + lowerLevel = level; + } + } + + assert(upperLevel <= queryLevel && + queryLevel <= lowerLevel); + + std::string joins, comparisons; + + size_t count = 0; + + for (size_t i = 0; i < lookup.size(); i++) + { + std::string comparison; + + if (FormatComparison(comparison, formatter, lookup[i], count)) + { + std::string join; + FormatJoin(join, lookup[i], count); + joins += join; + + if (!comparison.empty()) + { + comparisons += " AND " + comparison; + } + + count ++; + } + } + + sql = ("SELECT " + + FormatLevel(queryLevel) + ".publicId, " + + FormatLevel(queryLevel) + ".internalId" + + " FROM Resources AS " + FormatLevel(queryLevel)); + + for (int level = queryLevel - 1; level >= upperLevel; level--) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast(level)) + " ON " + + FormatLevel(static_cast(level)) + ".internalId=" + + FormatLevel(static_cast(level + 1)) + ".parentId"); + } + + for (int level = queryLevel + 1; level <= lowerLevel; level++) + { + sql += (" INNER JOIN Resources " + + FormatLevel(static_cast(level)) + " ON " + + FormatLevel(static_cast(level - 1)) + ".internalId=" + + FormatLevel(static_cast(level)) + ".parentId"); + } + + sql += (joins + " WHERE " + FormatLevel(queryLevel) + ".resourceType = " + + formatter.FormatResourceType(queryLevel) + comparisons); + + if (limit != 0) + { + sql += " LIMIT " + boost::lexical_cast(limit); + } + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/ISqlLookupFormatter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/ISqlLookupFormatter.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,65 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../../Core/Enumerations.h" + +#include +#include + +namespace Orthanc +{ + class DatabaseConstraint; + + // This class is also used by the "orthanc-databases" project + class ISqlLookupFormatter : public boost::noncopyable + { + public: + virtual ~ISqlLookupFormatter() + { + } + + virtual std::string GenerateParameter(const std::string& value) = 0; + + virtual std::string FormatResourceType(ResourceType level) = 0; + + virtual std::string FormatWildcardEscape() = 0; + + static void Apply(std::string& sql, + ISqlLookupFormatter& formatter, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit); + }; +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/ListConstraint.cpp --- a/OrthancServer/Search/ListConstraint.cpp Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "ListConstraint.h" - - -namespace Orthanc -{ - void ListConstraint::AddAllowedValue(const std::string& value) - { - if (isCaseSensitive_) - { - allowedValues_.insert(value); - } - else - { - allowedValues_.insert(Toolbox::ToUpperCaseWithAccents(value)); - } - } - - - void ListConstraint::Setup(LookupIdentifierQuery& lookup, - const DicomTag& tag) const - { - LookupIdentifierQuery::Disjunction& target = lookup.AddDisjunction(); - - for (std::set::const_iterator - it = allowedValues_.begin(); it != allowedValues_.end(); ++it) - { - target.Add(tag, IdentifierConstraintType_Equal, *it); - } - } - - - bool ListConstraint::Match(const std::string& value) const - { - std::string s; - - if (isCaseSensitive_) - { - s = value; - } - else - { - s = Toolbox::ToUpperCaseWithAccents(value); - } - - return allowedValues_.find(s) != allowedValues_.end(); - } - - - std::string ListConstraint::Format() const - { - std::string s; - - for (std::set::const_iterator - it = allowedValues_.begin(); it != allowedValues_.end(); ++it) - { - if (!s.empty()) - { - s += "\\"; - } - - s += *it; - } - - return s; - } -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/ListConstraint.h --- a/OrthancServer/Search/ListConstraint.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,74 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "IFindConstraint.h" - -#include - -namespace Orthanc -{ - class ListConstraint : public IFindConstraint - { - private: - std::set allowedValues_; - bool isCaseSensitive_; - - ListConstraint(const ListConstraint& other) : - allowedValues_(other.allowedValues_), - isCaseSensitive_(other.isCaseSensitive_) - { - } - - public: - ListConstraint(bool isCaseSensitive) : - isCaseSensitive_(isCaseSensitive) - { - } - - void AddAllowedValue(const std::string& value); - - virtual IFindConstraint* Clone() const - { - return new ListConstraint(*this); - } - - virtual void Setup(LookupIdentifierQuery& lookup, - const DicomTag& tag) const; - - virtual bool Match(const std::string& value) const; - - virtual std::string Format() const; - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/LookupIdentifierQuery.cpp --- a/OrthancServer/Search/LookupIdentifierQuery.cpp Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,215 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "LookupIdentifierQuery.h" - -#include "../../Core/DicomParsing/FromDcmtkBridge.h" -#include "../../Core/OrthancException.h" -#include "../ServerToolbox.h" -#include "SetOfResources.h" - -#include - - - -namespace Orthanc -{ - LookupIdentifierQuery::SingleConstraint:: - SingleConstraint(const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value) : - tag_(tag), - type_(type), - value_(ServerToolbox::NormalizeIdentifier(value)) - { - } - - - LookupIdentifierQuery::RangeConstraint:: - RangeConstraint(const DicomTag& tag, - const std::string& start, - const std::string& end) : - tag_(tag), - start_(ServerToolbox::NormalizeIdentifier(start)), - end_(ServerToolbox::NormalizeIdentifier(end)) - { - } - - - LookupIdentifierQuery::Disjunction::~Disjunction() - { - for (size_t i = 0; i < singleConstraints_.size(); i++) - { - delete singleConstraints_[i]; - } - - for (size_t i = 0; i < rangeConstraints_.size(); i++) - { - delete rangeConstraints_[i]; - } - } - - - void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value) - { - singleConstraints_.push_back(new SingleConstraint(tag, type, value)); - } - - - void LookupIdentifierQuery::Disjunction::AddRange(const DicomTag& tag, - const std::string& start, - const std::string& end) - { - rangeConstraints_.push_back(new RangeConstraint(tag, start, end)); - } - - - LookupIdentifierQuery::~LookupIdentifierQuery() - { - for (Disjunctions::iterator it = disjunctions_.begin(); - it != disjunctions_.end(); ++it) - { - delete *it; - } - } - - - bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag) - { - return ServerToolbox::IsIdentifier(tag, level_); - } - - - void LookupIdentifierQuery::AddConstraint(DicomTag tag, - IdentifierConstraintType type, - const std::string& value) - { - assert(IsIdentifier(tag)); - disjunctions_.push_back(new Disjunction); - disjunctions_.back()->Add(tag, type, value); - } - - - void LookupIdentifierQuery::AddRange(DicomTag tag, - const std::string& start, - const std::string& end) - { - assert(IsIdentifier(tag)); - disjunctions_.push_back(new Disjunction); - disjunctions_.back()->AddRange(tag, start, end); - } - - - LookupIdentifierQuery::Disjunction& LookupIdentifierQuery::AddDisjunction() - { - disjunctions_.push_back(new Disjunction); - return *disjunctions_.back(); - } - - - void LookupIdentifierQuery::Apply(std::list& result, - IDatabaseWrapper& database) - { - SetOfResources resources(database, level_); - Apply(resources, database); - - resources.Flatten(result); - } - - - void LookupIdentifierQuery::Apply(SetOfResources& result, - IDatabaseWrapper& database) - { - for (size_t i = 0; i < disjunctions_.size(); i++) - { - std::list a; - - for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++) - { - const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j); - std::list b; - database.LookupIdentifier(b, level_, constraint.GetTag(), - constraint.GetType(), constraint.GetValue()); - - a.splice(a.end(), b); - } - - for (size_t j = 0; j < disjunctions_[i]->GetRangeConstraintsCount(); j++) - { - const RangeConstraint& constraint = disjunctions_[i]->GetRangeConstraint(j); - std::list b; - database.LookupIdentifierRange(b, level_, constraint.GetTag(), - constraint.GetStart(), constraint.GetEnd()); - - a.splice(a.end(), b); - } - - result.Intersect(a); - } - } - - - void LookupIdentifierQuery::Print(std::ostream& s) const - { - s << "Constraint: " << std::endl; - for (Disjunctions::const_iterator - it = disjunctions_.begin(); it != disjunctions_.end(); ++it) - { - if (it == disjunctions_.begin()) - s << " "; - else - s << "OR "; - - for (size_t j = 0; j < (*it)->GetSingleConstraintsCount(); j++) - { - const SingleConstraint& c = (*it)->GetSingleConstraint(j); - s << FromDcmtkBridge::GetTagName(c.GetTag(), ""); - - switch (c.GetType()) - { - case IdentifierConstraintType_Equal: s << " == "; break; - case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break; - case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break; - case IdentifierConstraintType_Wildcard: s << " ~= "; break; - default: - s << " ? "; - } - - s << c.GetValue() << std::endl; - } - } - } -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/LookupIdentifierQuery.h --- a/OrthancServer/Search/LookupIdentifierQuery.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,207 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "../IDatabaseWrapper.h" - -#include "SetOfResources.h" - -#include -#include - -namespace Orthanc -{ - /** - * Primitive for wildcard matching, as defined in DICOM: - * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4 - * - * "Any occurrence of an "*" or a "?", then "*" shall match any - * sequence of characters (including a zero length value) and "?" - * shall match any single character. This matching is case - * sensitive, except for Attributes with an PN Value - * Representation (e.g., Patient Name (0010,0010))." - * - * Pay attention to the fact that "*" (resp. "?") generally - * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The - * values "%", "_", "\" should in the user request should - * respectively be escaped as "\%", "\_" and "\\". - * - * This matching must be case sensitive: The special case of PN VR - * is taken into consideration by normalizing the query string in - * method "NormalizeIdentifier()". - **/ - - class LookupIdentifierQuery : public boost::noncopyable - { - // This class encodes a conjunction ("AND") of disjunctions. Each - // disjunction represents an "OR" of several constraints. - - public: - class SingleConstraint - { - private: - DicomTag tag_; - IdentifierConstraintType type_; - std::string value_; - - public: - SingleConstraint(const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value); - - const DicomTag& GetTag() const - { - return tag_; - } - - IdentifierConstraintType GetType() const - { - return type_; - } - - const std::string& GetValue() const - { - return value_; - } - }; - - - class RangeConstraint - { - private: - DicomTag tag_; - std::string start_; - std::string end_; - - public: - RangeConstraint(const DicomTag& tag, - const std::string& start, - const std::string& end); - - const DicomTag& GetTag() const - { - return tag_; - } - - const std::string& GetStart() const - { - return start_; - } - - const std::string& GetEnd() const - { - return end_; - } - }; - - - class Disjunction : public boost::noncopyable - { - private: - std::vector singleConstraints_; - std::vector rangeConstraints_; - - public: - ~Disjunction(); - - void Add(const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value); - - void AddRange(const DicomTag& tag, - const std::string& start, - const std::string& end); - - size_t GetSingleConstraintsCount() const - { - return singleConstraints_.size(); - } - - const SingleConstraint& GetSingleConstraint(size_t i) const - { - return *singleConstraints_[i]; - } - - size_t GetRangeConstraintsCount() const - { - return rangeConstraints_.size(); - } - - const RangeConstraint& GetRangeConstraint(size_t i) const - { - return *rangeConstraints_[i]; - } - }; - - - private: - typedef std::vector Disjunctions; - - ResourceType level_; - Disjunctions disjunctions_; - - public: - LookupIdentifierQuery(ResourceType level) : level_(level) - { - } - - ~LookupIdentifierQuery(); - - bool IsIdentifier(const DicomTag& tag); - - void AddConstraint(DicomTag tag, - IdentifierConstraintType type, - const std::string& value); - - void AddRange(DicomTag tag, - const std::string& start, - const std::string& end); - - Disjunction& AddDisjunction(); - - ResourceType GetLevel() const - { - return level_; - } - - // The database must be locked - void Apply(std::list& result, - IDatabaseWrapper& database); - - void Apply(SetOfResources& result, - IDatabaseWrapper& database); - - void Print(std::ostream& s) const; - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/LookupResource.cpp --- a/OrthancServer/Search/LookupResource.cpp Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,479 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "LookupResource.h" - -#include "../../Core/OrthancException.h" -#include "../../Core/FileStorage/StorageAccessor.h" -#include "../ServerToolbox.h" -#include "../../Core/DicomParsing/FromDcmtkBridge.h" - - -namespace Orthanc -{ - static bool DoesDicomMapMatch(const DicomMap& dicom, - const DicomTag& tag, - const IFindConstraint& constraint) - { - const DicomValue* value = dicom.TestAndGetValue(tag); - - return (value != NULL && - !value->IsNull() && - !value->IsBinary() && - constraint.Match(value->GetContent())); - } - - - LookupResource::Level::Level(ResourceType level) : level_(level) - { - const DicomTag* tags = NULL; - size_t size; - - ServerToolbox::LoadIdentifiers(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - identifiers_.insert(tags[i]); - } - - DicomMap::LoadMainDicomTags(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - if (identifiers_.find(tags[i]) == identifiers_.end()) - { - mainTags_.insert(tags[i]); - } - } - } - - LookupResource::Level::~Level() - { - for (Constraints::iterator it = mainTagsConstraints_.begin(); - it != mainTagsConstraints_.end(); ++it) - { - delete it->second; - } - - for (Constraints::iterator it = identifiersConstraints_.begin(); - it != identifiersConstraints_.end(); ++it) - { - delete it->second; - } - } - - bool LookupResource::Level::Add(const DicomTag& tag, - std::auto_ptr& constraint) - { - if (identifiers_.find(tag) != identifiers_.end()) - { - if (level_ == ResourceType_Patient) - { - // The filters on the patient level must be cloned to the study level - identifiersConstraints_[tag] = constraint->Clone(); - } - else - { - identifiersConstraints_[tag] = constraint.release(); - } - - return true; - } - else if (mainTags_.find(tag) != mainTags_.end()) - { - if (level_ == ResourceType_Patient) - { - // The filters on the patient level must be cloned to the study level - mainTagsConstraints_[tag] = constraint->Clone(); - } - else - { - mainTagsConstraints_[tag] = constraint.release(); - } - - return true; - } - else - { - // This is not a main DICOM tag - return false; - } - } - - - bool LookupResource::Level::IsMatch(const DicomMap& dicom) const - { - for (Constraints::const_iterator it = identifiersConstraints_.begin(); - it != identifiersConstraints_.end(); ++it) - { - assert(it->second != NULL); - - if (!DoesDicomMapMatch(dicom, it->first, *it->second)) - { - return false; - } - } - - for (Constraints::const_iterator it = mainTagsConstraints_.begin(); - it != mainTagsConstraints_.end(); ++it) - { - assert(it->second != NULL); - - if (!DoesDicomMapMatch(dicom, it->first, *it->second)) - { - return false; - } - } - - return true; - } - - - LookupResource::LookupResource(ResourceType level) : level_(level) - { - switch (level) - { - case ResourceType_Patient: - levels_[ResourceType_Patient] = new Level(ResourceType_Patient); - break; - - case ResourceType_Instance: - levels_[ResourceType_Instance] = new Level(ResourceType_Instance); - // Do not add "break" here - - case ResourceType_Series: - levels_[ResourceType_Series] = new Level(ResourceType_Series); - // Do not add "break" here - - case ResourceType_Study: - levels_[ResourceType_Study] = new Level(ResourceType_Study); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - - LookupResource::~LookupResource() - { - for (Levels::iterator it = levels_.begin(); - it != levels_.end(); ++it) - { - delete it->second; - } - - for (Constraints::iterator it = unoptimizedConstraints_.begin(); - it != unoptimizedConstraints_.end(); ++it) - { - delete it->second; - } - } - - - - bool LookupResource::AddInternal(ResourceType level, - const DicomTag& tag, - std::auto_ptr& constraint) - { - Levels::iterator it = levels_.find(level); - if (it != levels_.end()) - { - if (it->second->Add(tag, constraint)) - { - return true; - } - } - - return false; - } - - - void LookupResource::Add(const DicomTag& tag, - IFindConstraint* constraint) - { - std::auto_ptr c(constraint); - - if (!AddInternal(ResourceType_Patient, tag, c) && - !AddInternal(ResourceType_Study, tag, c) && - !AddInternal(ResourceType_Series, tag, c) && - !AddInternal(ResourceType_Instance, tag, c)) - { - unoptimizedConstraints_[tag] = c.release(); - } - } - - - static bool Match(const DicomMap& tags, - const DicomTag& tag, - const IFindConstraint& constraint) - { - const DicomValue* value = tags.TestAndGetValue(tag); - - if (value == NULL || - value->IsNull() || - value->IsBinary()) - { - return false; - } - else - { - return constraint.Match(value->GetContent()); - } - } - - - void LookupResource::Level::Apply(SetOfResources& candidates, - IDatabaseWrapper& database) const - { - // First, use the indexed identifiers - LookupIdentifierQuery query(level_); - - for (Constraints::const_iterator it = identifiersConstraints_.begin(); - it != identifiersConstraints_.end(); ++it) - { - it->second->Setup(query, it->first); - } - - query.Apply(candidates, database); - - /*{ - query.Print(std::cout); - std::list source; - candidates.Flatten(source); - printf("=> %d\n", source.size()); - }*/ - - // Secondly, filter using the main DICOM tags - if (!identifiersConstraints_.empty() || - !mainTagsConstraints_.empty()) - { - std::list source; - candidates.Flatten(source); - candidates.Clear(); - - std::list filtered; - for (std::list::const_iterator candidate = source.begin(); - candidate != source.end(); ++candidate) - { - DicomMap tags; - database.GetMainDicomTags(tags, *candidate); - - bool match = true; - - // Re-apply the identifier constraints, as their "Setup" - // method is less restrictive than their "Match" method - for (Constraints::const_iterator it = identifiersConstraints_.begin(); - match && it != identifiersConstraints_.end(); ++it) - { - if (!Match(tags, it->first, *it->second)) - { - match = false; - } - } - - for (Constraints::const_iterator it = mainTagsConstraints_.begin(); - match && it != mainTagsConstraints_.end(); ++it) - { - if (!Match(tags, it->first, *it->second)) - { - match = false; - } - } - - if (match) - { - filtered.push_back(*candidate); - } - } - - candidates.Intersect(filtered); - } - } - - - - bool LookupResource::IsMatch(const DicomMap& dicom) const - { - for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it) - { - if (!it->second->IsMatch(dicom)) - { - return false; - } - } - - for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); - it != unoptimizedConstraints_.end(); ++it) - { - assert(it->second != NULL); - - if (!DoesDicomMapMatch(dicom, it->first, *it->second)) - { - return false; - } - } - - return true; - } - - - void LookupResource::ApplyLevel(SetOfResources& candidates, - ResourceType level, - IDatabaseWrapper& database) const - { - Levels::const_iterator it = levels_.find(level); - if (it != levels_.end()) - { - it->second->Apply(candidates, database); - } - - if (level == ResourceType_Study && - modalitiesInStudy_.get() != NULL) - { - // There is a constraint on the "ModalitiesInStudy" DICOM - // extension. Check out whether one child series has one of the - // allowed modalities - std::list allStudies, matchingStudies; - candidates.Flatten(allStudies); - - for (std::list::const_iterator - study = allStudies.begin(); study != allStudies.end(); ++study) - { - std::list childrenSeries; - database.GetChildrenInternalId(childrenSeries, *study); - - for (std::list::const_iterator - series = childrenSeries.begin(); series != childrenSeries.end(); ++series) - { - DicomMap tags; - database.GetMainDicomTags(tags, *series); - - const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) - { - if (modalitiesInStudy_->Match(value->GetContent())) - { - matchingStudies.push_back(*study); - break; - } - } - } - } - - candidates.Intersect(matchingStudies); - } - } - - - void LookupResource::FindCandidates(std::list& result, - IDatabaseWrapper& database) const - { - ResourceType startingLevel; - if (level_ == ResourceType_Patient) - { - startingLevel = ResourceType_Patient; - } - else - { - startingLevel = ResourceType_Study; - } - - SetOfResources candidates(database, startingLevel); - - switch (level_) - { - case ResourceType_Patient: - ApplyLevel(candidates, ResourceType_Patient, database); - break; - - case ResourceType_Study: - ApplyLevel(candidates, ResourceType_Study, database); - break; - - case ResourceType_Series: - ApplyLevel(candidates, ResourceType_Study, database); - candidates.GoDown(); - ApplyLevel(candidates, ResourceType_Series, database); - break; - - case ResourceType_Instance: - ApplyLevel(candidates, ResourceType_Study, database); - candidates.GoDown(); - ApplyLevel(candidates, ResourceType_Series, database); - candidates.GoDown(); - ApplyLevel(candidates, ResourceType_Instance, database); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - candidates.Flatten(result); - } - - - void LookupResource::SetModalitiesInStudy(const std::string& modalities) - { - modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */)); - - std::vector items; - Toolbox::TokenizeString(items, modalities, '\\'); - - for (size_t i = 0; i < items.size(); i++) - { - modalitiesInStudy_->AddAllowedValue(items[i]); - } - } - - - void LookupResource::AddDicomConstraint(const DicomTag& tag, - const std::string& dicomQuery, - bool caseSensitive) - { - // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained - // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html - if (tag == DICOM_TAG_MODALITIES_IN_STUDY) - { - SetModalitiesInStudy(dicomQuery); - } - else - { - Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive)); - } - } - -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/LookupResource.h --- a/OrthancServer/Search/LookupResource.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "ListConstraint.h" -#include "SetOfResources.h" - -#include - -namespace Orthanc -{ - class LookupResource : public boost::noncopyable - { - private: - typedef std::map Constraints; - - class Level - { - private: - ResourceType level_; - std::set identifiers_; - std::set mainTags_; - Constraints identifiersConstraints_; - Constraints mainTagsConstraints_; - - public: - Level(ResourceType level); - - ~Level(); - - bool Add(const DicomTag& tag, - std::auto_ptr& constraint); - - void Apply(SetOfResources& candidates, - IDatabaseWrapper& database) const; - - bool IsMatch(const DicomMap& dicom) const; - }; - - typedef std::map Levels; - - ResourceType level_; - Levels levels_; - Constraints unoptimizedConstraints_; // Constraints on non-main DICOM tags - std::auto_ptr modalitiesInStudy_; - - bool AddInternal(ResourceType level, - const DicomTag& tag, - std::auto_ptr& constraint); - - void ApplyLevel(SetOfResources& candidates, - ResourceType level, - IDatabaseWrapper& database) const; - - public: - LookupResource(ResourceType level); - - ~LookupResource(); - - ResourceType GetLevel() const - { - return level_; - } - - void SetModalitiesInStudy(const std::string& modalities); - - void Add(const DicomTag& tag, - IFindConstraint* constraint); // Takes ownership - - void AddDicomConstraint(const DicomTag& tag, - const std::string& dicomQuery, - bool caseSensitive); - - void FindCandidates(std::list& result, - IDatabaseWrapper& database) const; - - bool HasOnlyMainDicomTags() const - { - return unoptimizedConstraints_.empty(); - } - - bool IsMatch(const DicomMap& dicom) const; - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/RangeConstraint.cpp --- a/OrthancServer/Search/RangeConstraint.cpp Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "RangeConstraint.h" - -#include "../../Core/Toolbox.h" - -namespace Orthanc -{ - RangeConstraint::RangeConstraint(const std::string& lower, - const std::string& upper, - bool isCaseSensitive) : - isCaseSensitive_(isCaseSensitive) - { - if (isCaseSensitive_) - { - lower_ = lower; - upper_ = upper; - } - else - { - lower_ = Toolbox::ToUpperCaseWithAccents(lower); - upper_ = Toolbox::ToUpperCaseWithAccents(upper); - } - } - - - void RangeConstraint::Setup(LookupIdentifierQuery& lookup, - const DicomTag& tag) const - { - if (!lower_.empty() && - !upper_.empty()) - { - lookup.AddRange(tag, lower_, upper_); - } - else - { - if (!lower_.empty()) - { - lookup.AddConstraint(tag, IdentifierConstraintType_GreaterOrEqual, lower_); - } - - if (!upper_.empty()) - { - lookup.AddConstraint(tag, IdentifierConstraintType_SmallerOrEqual, upper_); - } - } - } - - - bool RangeConstraint::Match(const std::string& value) const - { - std::string v; - - if (isCaseSensitive_) - { - v = value; - } - else - { - v = Toolbox::ToUpperCaseWithAccents(value); - } - - if (lower_.size() == 0 && - upper_.size() == 0) - { - return false; - } - - if (lower_.size() == 0) - { - return v <= upper_; - } - - if (upper_.size() == 0) - { - return v >= lower_; - } - - return (v >= lower_ && v <= upper_); - } -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/RangeConstraint.h --- a/OrthancServer/Search/RangeConstraint.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,74 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "IFindConstraint.h" - -namespace Orthanc -{ - class RangeConstraint : public IFindConstraint - { - private: - std::string lower_; - std::string upper_; - bool isCaseSensitive_; - - RangeConstraint(const RangeConstraint& other) : - lower_(other.lower_), - upper_(other.upper_), - isCaseSensitive_(other.isCaseSensitive_) - { - } - - public: - RangeConstraint(const std::string& lower, - const std::string& upper, - bool isCaseSensitive); - - virtual IFindConstraint* Clone() const - { - return new RangeConstraint(*this); - } - - virtual void Setup(LookupIdentifierQuery& lookup, - const DicomTag& tag) const; - - virtual bool Match(const std::string& value) const; - - virtual std::string Format() const - { - return lower_ + "-" + upper_; - } - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/SetOfResources.cpp --- a/OrthancServer/Search/SetOfResources.cpp Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,157 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "SetOfResources.h" - -#include "../../Core/OrthancException.h" - - -namespace Orthanc -{ - void SetOfResources::Intersect(const std::list& resources) - { - if (resources_.get() == NULL) - { - resources_.reset(new Resources); - - for (std::list::const_iterator - it = resources.begin(); it != resources.end(); ++it) - { - resources_->insert(*it); - } - } - else - { - std::auto_ptr filtered(new Resources); - - for (std::list::const_iterator - it = resources.begin(); it != resources.end(); ++it) - { - if (resources_->find(*it) != resources_->end()) - { - filtered->insert(*it); - } - } - - resources_ = filtered; - } - } - - - void SetOfResources::GoDown() - { - if (level_ == ResourceType_Instance) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - if (resources_.get() != NULL) - { - std::auto_ptr children(new Resources); - - for (Resources::const_iterator it = resources_->begin(); - it != resources_->end(); ++it) - { - std::list tmp; - database_.GetChildrenInternalId(tmp, *it); - - for (std::list::const_iterator - child = tmp.begin(); child != tmp.end(); ++child) - { - children->insert(*child); - } - } - - resources_ = children; - } - - switch (level_) - { - case ResourceType_Patient: - level_ = ResourceType_Study; - break; - - case ResourceType_Study: - level_ = ResourceType_Series; - break; - - case ResourceType_Series: - level_ = ResourceType_Instance; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - - void SetOfResources::Flatten(std::list& result) - { - result.clear(); - - if (resources_.get() == NULL) - { - // All the resources of this level are part of the filter - database_.GetAllPublicIds(result, level_); - } - else - { - for (Resources::const_iterator it = resources_->begin(); - it != resources_->end(); ++it) - { - result.push_back(database_.GetPublicId(*it)); - } - } - } - - - void SetOfResources::Flatten(std::list& result) - { - result.clear(); - - if (resources_.get() == NULL) - { - // All the resources of this level are part of the filter - database_.GetAllInternalIds(result, level_); - } - else - { - for (Resources::const_iterator it = resources_->begin(); - it != resources_->end(); ++it) - { - result.push_back(*it); - } - } - } -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/SetOfResources.h --- a/OrthancServer/Search/SetOfResources.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,79 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "../IDatabaseWrapper.h" - -#include -#include -#include - -namespace Orthanc -{ - class SetOfResources : public boost::noncopyable - { - private: - typedef std::set Resources; - - IDatabaseWrapper& database_; - ResourceType level_; - std::auto_ptr resources_; - - public: - SetOfResources(IDatabaseWrapper& database, - ResourceType level) : - database_(database), - level_(level) - { - } - - ResourceType GetLevel() const - { - return level_; - } - - void Intersect(const std::list& resources); - - void GoDown(); - - void Flatten(std::list& result); - - void Flatten(std::list& result); - - void Clear() - { - resources_.reset(NULL); - } - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/ValueConstraint.cpp --- a/OrthancServer/Search/ValueConstraint.cpp Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,75 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "ValueConstraint.h" - -#include "../../Core/Toolbox.h" - -#include - -namespace Orthanc -{ - ValueConstraint::ValueConstraint(const std::string& value, - bool isCaseSensitive) : - isCaseSensitive_(isCaseSensitive) - { - if (isCaseSensitive) - { - value_ = value; - } - else - { - value_ = Toolbox::ToUpperCaseWithAccents(value); - } - } - - - void ValueConstraint::Setup(LookupIdentifierQuery& lookup, - const DicomTag& tag) const - { - lookup.AddConstraint(tag, IdentifierConstraintType_Equal, value_); - } - - bool ValueConstraint::Match(const std::string& value) const - { - if (isCaseSensitive_) - { - return value_ == value; - } - else - { - return value_ == Toolbox::ToUpperCaseWithAccents(value); - } - } -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/ValueConstraint.h --- a/OrthancServer/Search/ValueConstraint.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "IFindConstraint.h" - -namespace Orthanc -{ - class ValueConstraint : public IFindConstraint - { - private: - std::string value_; - bool isCaseSensitive_; - - ValueConstraint(const ValueConstraint& other) : - value_(other.value_), - isCaseSensitive_(other.isCaseSensitive_) - { - } - - public: - ValueConstraint(const std::string& value, - bool isCaseSensitive); - - virtual IFindConstraint* Clone() const - { - return new ValueConstraint(*this); - } - - virtual void Setup(LookupIdentifierQuery& lookup, - const DicomTag& tag) const; - - virtual bool Match(const std::string& value) const; - - virtual std::string Format() const - { - return value_; - } - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/WildcardConstraint.cpp --- a/OrthancServer/Search/WildcardConstraint.cpp Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "WildcardConstraint.h" - -#include - -namespace Orthanc -{ - struct WildcardConstraint::PImpl - { - boost::regex pattern_; - std::string wildcard_; - bool isCaseSensitive_; - - PImpl(const std::string& wildcard, - bool isCaseSensitive) - { - isCaseSensitive_ = isCaseSensitive; - - if (isCaseSensitive) - { - wildcard_ = wildcard; - } - else - { - wildcard_ = Toolbox::ToUpperCaseWithAccents(wildcard); - } - - pattern_ = boost::regex(Toolbox::WildcardToRegularExpression(wildcard_)); - } - }; - - - WildcardConstraint::WildcardConstraint(const WildcardConstraint& other) : - pimpl_(new PImpl(*other.pimpl_)) - { - } - - - WildcardConstraint::WildcardConstraint(const std::string& wildcard, - bool isCaseSensitive) : - pimpl_(new PImpl(wildcard, isCaseSensitive)) - { - } - - bool WildcardConstraint::Match(const std::string& value) const - { - if (pimpl_->isCaseSensitive_) - { - return boost::regex_match(value, pimpl_->pattern_); - } - else - { - return boost::regex_match(Toolbox::ToUpperCaseWithAccents(value), pimpl_->pattern_); - } - } - - void WildcardConstraint::Setup(LookupIdentifierQuery& lookup, - const DicomTag& tag) const - { - lookup.AddConstraint(tag, IdentifierConstraintType_Wildcard, pimpl_->wildcard_); - } - - std::string WildcardConstraint::Format() const - { - return pimpl_->wildcard_; - } -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Search/WildcardConstraint.h --- a/OrthancServer/Search/WildcardConstraint.h Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,66 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2019 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 "IFindConstraint.h" - -#include - -namespace Orthanc -{ - class WildcardConstraint : public IFindConstraint - { - private: - struct PImpl; - boost::shared_ptr pimpl_; - - WildcardConstraint(const WildcardConstraint& other); - - public: - WildcardConstraint(const std::string& wildcard, - bool isCaseSensitive); - - virtual IFindConstraint* Clone() const - { - return new WildcardConstraint(*this); - } - - virtual void Setup(LookupIdentifierQuery& lookup, - const DicomTag& tag) const; - - virtual bool Match(const std::string& value) const; - - virtual std::string Format() const; - }; -} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerContext.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -34,15 +34,18 @@ #include "PrecompiledHeadersServer.h" #include "ServerContext.h" +#include "../Core/Cache/SharedArchive.h" #include "../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/FileStorage/StorageAccessor.h" #include "../Core/HttpServer/FilesystemHttpSender.h" #include "../Core/HttpServer/HttpStreamTranscoder.h" +#include "../Core/JobsEngine/SetOfInstancesJob.h" #include "../Core/Logging.h" #include "../Plugins/Engine/OrthancPlugins.h" + #include "OrthancConfiguration.h" #include "OrthancRestApi/OrthancRestApi.h" -#include "Search/LookupResource.h" +#include "Search/DatabaseLookup.h" #include "ServerJobs/OrthancJobUnserializer.h" #include "ServerToolbox.h" @@ -774,11 +777,13 @@ void ServerContext::Apply(ILookupVisitor& visitor, - const ::Orthanc::LookupResource& lookup, + const DatabaseLookup& lookup, + ResourceType queryLevel, size_t since, size_t limit) { LookupMode mode; + unsigned int databaseLimit; { // New configuration option in 1.5.1 @@ -804,11 +809,24 @@ "Configuration option \"StorageAccessOnFind\" " "should be \"Always\", \"Never\" or \"Answers\": " + value); } + + if (queryLevel == ResourceType_Instance) + { + databaseLimit = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0); + } + else + { + databaseLimit = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0); + } } + std::vector resources, instances; - std::vector resources, instances; - GetIndex().FindCandidates(resources, instances, lookup); + const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1); + GetIndex().ApplyLookupResources(resources, &instances, lookup, queryLevel, lookupLimit); + + bool complete = (databaseLimit == 0 || + resources.size() > databaseLimit); LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size(); @@ -816,7 +834,6 @@ size_t countResults = 0; size_t skipped = 0; - bool complete = true; const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded(); diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerContext.h Thu Jan 24 10:55:19 2019 +0100 @@ -33,29 +33,27 @@ #pragma once -#include "DicomInstanceToStore.h" #include "IServerListener.h" #include "LuaScripting.h" #include "OrthancHttpHandler.h" #include "ServerIndex.h" -#include "Search/LookupResource.h" #include "../Core/Cache/MemoryCache.h" -#include "../Core/Cache/SharedArchive.h" -#include "../Core/DicomParsing/ParsedDicomFile.h" -#include "../Core/FileStorage/IStorageArea.h" -#include "../Core/JobsEngine/JobsEngine.h" -#include "../Core/JobsEngine/SetOfInstancesJob.h" -#include "../Core/MultiThreading/SharedMessageQueue.h" -#include "../Core/RestApi/RestApiOutput.h" -#include "../Plugins/Engine/OrthancPlugins.h" - -#include -#include namespace Orthanc { + class DicomInstanceToStore; + class IStorageArea; + class JobsEngine; + class OrthancPlugins; + class ParsedDicomFile; + class RestApiOutput; + class SetOfInstancesJob; + class SharedArchive; + class SharedMessageQueue; + + /** * This class is responsible for maintaining the storage area on the * filesystem (including compression), as well as the index of the @@ -363,7 +361,8 @@ void Stop(); void Apply(ILookupVisitor& visitor, - const ::Orthanc::LookupResource& lookup, + const DatabaseLookup& lookup, + ResourceType queryLevel, size_t since, size_t limit); diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerEnumerations.h Thu Jan 24 10:55:19 2019 +0100 @@ -56,14 +56,6 @@ StoreStatus_FilteredOut // Removed by NewInstanceFilter }; - enum IdentifierConstraintType - { - IdentifierConstraintType_Equal, - IdentifierConstraintType_SmallerOrEqual, - IdentifierConstraintType_GreaterOrEqual, - 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 @@ -80,6 +72,17 @@ ConstraintType_List }; + namespace Compatibility + { + enum IdentifierConstraintType + { + IdentifierConstraintType_Equal, + IdentifierConstraintType_SmallerOrEqual, + IdentifierConstraintType_GreaterOrEqual, + IdentifierConstraintType_Wildcard /* Case sensitive, "*" or "?" are the only allowed wildcards */ + }; + } + /** * WARNING: Do not change the explicit values in the enumerations @@ -93,8 +96,7 @@ GlobalProperty_FlushSleep = 2, GlobalProperty_AnonymizationSequence = 3, GlobalProperty_JobsRegistry = 5, - GlobalProperty_TotalCompressedSize = 6, // Reserved for Orthanc > 1.5.0 - GlobalProperty_TotalUncompressedSize = 7, // Reserved for Orthanc > 1.5.0 + GlobalProperty_GetTotalSizeIsFast = 6, // New in Orthanc 1.5.2 GlobalProperty_Modalities = 20, // New in Orthanc 1.5.0 GlobalProperty_Peers = 21, // New in Orthanc 1.5.0 diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerIndex.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -38,19 +38,21 @@ #define NOMINMAX #endif -#include "ServerIndexChange.h" +#include "../Core/DicomFormat/DicomArray.h" +#include "../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Core/DicomParsing/ParsedDicomFile.h" +#include "../Core/Logging.h" +#include "../Core/Toolbox.h" + +#include "Database/ResourcesContent.h" +#include "DicomInstanceToStore.h" #include "EmbeddedResources.h" #include "OrthancConfiguration.h" -#include "../Core/DicomParsing/ParsedDicomFile.h" +#include "Search/DatabaseLookup.h" +#include "Search/DicomTagConstraint.h" +#include "ServerContext.h" +#include "ServerIndexChange.h" #include "ServerToolbox.h" -#include "../Core/Toolbox.h" -#include "../Core/Logging.h" -#include "../Core/DicomFormat/DicomArray.h" - -#include "../Core/DicomParsing/FromDcmtkBridge.h" -#include "ServerContext.h" -#include "DicomInstanceToStore.h" -#include "Search/LookupResource.h" #include #include @@ -59,6 +61,22 @@ namespace Orthanc { + static void CopyListToVector(std::vector& target, + const std::list& source) + { + target.resize(source.size()); + + size_t pos = 0; + + for (std::list::const_iterator + it = source.begin(); it != source.end(); ++it) + { + target[pos] = *it; + pos ++; + } + } + + class ServerIndex::Listener : public IDatabaseListener { private: @@ -215,7 +233,7 @@ { private: ServerIndex& index_; - std::auto_ptr transaction_; + std::auto_ptr transaction_; bool isCommitted_; public: @@ -226,8 +244,6 @@ transaction_.reset(index_.db_.StartTransaction()); transaction_->Begin(); - assert(index_.currentStorageSize_ == index_.db_.GetTotalCompressedSize()); - index_.listener_->StartTransaction(); } @@ -245,18 +261,16 @@ { if (!isCommitted_) { - transaction_->Commit(); + int64_t delta = (static_cast(sizeOfAddedFiles) - + static_cast(index_.listener_->GetSizeOfFilesToRemove())); + + transaction_->Commit(delta); // We can remove the files once the SQLite transaction has // been successfully committed. Some files might have to be // deleted because of recycling. index_.listener_->CommitFilesToRemove(); - index_.currentStorageSize_ += sizeOfAddedFiles; - - assert(index_.currentStorageSize_ >= index_.listener_->GetSizeOfFilesToRemove()); - index_.currentStorageSize_ -= index_.listener_->GetSizeOfFilesToRemove(); - // Send all the pending changes to the Orthanc plugins index_.listener_->CommitChanges(); @@ -303,6 +317,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) @@ -387,8 +502,7 @@ } - static void ComputeExpectedNumberOfInstances(IDatabaseWrapper& db, - int64_t series, + static bool ComputeExpectedNumberOfInstances(int64_t& target, const DicomMap& dicomSummary) { try @@ -397,28 +511,39 @@ const DicomValue* value2; if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL && - (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL) + !value->IsNull() && + !value->IsBinary() && + (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL && + !value2->IsNull() && + !value2->IsBinary()) { // Patch for series with temporal positions thanks to Will Ryder int64_t imagesInAcquisition = boost::lexical_cast(value->GetContent()); int64_t countTemporalPositions = boost::lexical_cast(value2->GetContent()); - std::string expected = boost::lexical_cast(imagesInAcquisition * countTemporalPositions); - db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, expected); + target = imagesInAcquisition * countTemporalPositions; + return (target > 0); } else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL && - (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL) + !value->IsNull() && + !value->IsBinary() && + (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL && + !value2->IsBinary() && + !value2->IsNull()) { // Support of Cardio-PET images int64_t numberOfSlices = boost::lexical_cast(value->GetContent()); int64_t numberOfTimeSlices = boost::lexical_cast(value2->GetContent()); - std::string expected = boost::lexical_cast(numberOfSlices * numberOfTimeSlices); - db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, expected); + target = numberOfSlices * numberOfTimeSlices; + return (target > 0); } - else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL) + else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL && + !value->IsNull() && + !value->IsBinary()) { - db.SetMetadata(series, MetadataType_Series_ExpectedNumberOfInstances, value->GetContent()); + target = boost::lexical_cast(value->GetContent()); + return (target > 0); } } catch (OrthancException&) @@ -427,6 +552,8 @@ catch (boost::bad_lexical_cast&) { } + + return false; } @@ -500,44 +627,6 @@ - int64_t ServerIndex::CreateResource(const std::string& publicId, - ResourceType type) - { - int64_t id = db_.CreateResource(publicId, type); - - ChangeType changeType; - switch (type) - { - case ResourceType_Patient: - changeType = ChangeType_NewPatient; - break; - - case ResourceType_Study: - changeType = ChangeType_NewStudy; - break; - - case ResourceType_Series: - changeType = ChangeType_NewSeries; - break; - - case ResourceType_Instance: - changeType = ChangeType_NewInstance; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - ServerIndexChange change(changeType, type, publicId); - db_.LogChange(id, change); - - assert(listener_.get() != NULL); - listener_->SignalChange(change); - - return id; - } - - ServerIndex::ServerIndex(ServerContext& context, IDatabaseWrapper& db, unsigned int threadSleep) : @@ -545,13 +634,12 @@ db_(db), maximumStorageSize_(0), maximumPatients_(0), - overwrite_(false) + overwrite_(false), + mainDicomTagsRegistry_(new MainDicomTagsRegistry) { listener_.reset(new Listener(context)); db_.SetListener(*listener_); - currentStorageSize_ = db_.GetTotalCompressedSize(); - // Initial recycling if the parameters have changed since the last // execution of Orthanc StandaloneRecycling(); @@ -598,18 +686,30 @@ } - - void ServerIndex::SetInstanceMetadata(std::map& instanceMetadata, - int64_t instance, - MetadataType metadata, - const std::string& value) + static void SetInstanceMetadata(ResourcesContent& content, + std::map& instanceMetadata, + int64_t instance, + MetadataType metadata, + const std::string& value) { - db_.SetMetadata(instance, metadata, value); + content.AddMetadata(instance, metadata, value); instanceMetadata[metadata] = value; } - + void ServerIndex::SignalNewResource(ChangeType changeType, + ResourceType level, + const std::string& publicId, + int64_t internalId) + { + ServerIndexChange change(changeType, level, publicId); + db_.LogChange(internalId, change); + + assert(listener_.get() != NULL); + listener_->SignalChange(change); + } + + StoreStatus ServerIndex::Store(std::map& instanceMetadata, DicomInstanceToStore& instanceToStore, const Attachments& attachments) @@ -619,35 +719,78 @@ const DicomMap& dicomSummary = instanceToStore.GetSummary(); const ServerIndex::MetadataMap& metadata = instanceToStore.GetMetadata(); + int64_t expectedInstances; + const bool hasExpectedInstances = + ComputeExpectedNumberOfInstances(expectedInstances, dicomSummary); + instanceMetadata.clear(); + const std::string hashPatient = instanceToStore.GetHasher().HashPatient(); + const std::string hashStudy = instanceToStore.GetHasher().HashStudy(); + const std::string hashSeries = instanceToStore.GetHasher().HashSeries(); + const std::string hashInstance = instanceToStore.GetHasher().HashInstance(); + try { Transaction t(*this); + IDatabaseWrapper::CreateInstanceResult status; + int64_t instanceId; + // Check whether this instance is already stored + if (!db_.CreateInstance(status, instanceId, hashPatient, + hashStudy, hashSeries, hashInstance)) { - ResourceType type; - int64_t tmp; - if (db_.LookupResource(tmp, type, instanceToStore.GetHasher().HashInstance())) + // The instance already exists + + if (overwrite_) { - assert(type == ResourceType_Instance); - - if (overwrite_) + // Overwrite the old instance + LOG(INFO) << "Overwriting instance: " << hashInstance; + db_.DeleteResource(instanceId); + + // Re-create the instance, now that the old one is removed + if (!db_.CreateInstance(status, instanceId, hashPatient, + hashStudy, hashSeries, hashInstance)) { - // Overwrite the old instance - LOG(INFO) << "Overwriting instance: " << instanceToStore.GetHasher().HashInstance(); - db_.DeleteResource(tmp); - } - else - { - // Do nothing if the instance already exists - db_.GetAllMetadata(instanceMetadata, tmp); - return StoreStatus_AlreadyStored; + throw OrthancException(ErrorCode_InternalError); } } + else + { + // Do nothing if the instance already exists and overwriting is disabled + db_.GetAllMetadata(instanceMetadata, instanceId); + return StoreStatus_AlreadyStored; + } } + + // Warn about the creation of new resources. The order must be + // from instance to patient. + + // NB: In theory, could be sped up by grouping the underlying + // calls to "db_.LogChange()". However, this would only have an + // impact when new patient/study/series get created, which + // occurs far less often that creating new instances. The + // positive impact looks marginal in practice. + SignalNewResource(ChangeType_NewInstance, ResourceType_Instance, hashInstance, instanceId); + + if (status.isNewSeries_) + { + SignalNewResource(ChangeType_NewSeries, ResourceType_Series, hashSeries, status.seriesId_); + } + + if (status.isNewStudy_) + { + SignalNewResource(ChangeType_NewStudy, ResourceType_Study, hashStudy, status.studyId_); + } + + if (status.isNewPatient_) + { + SignalNewResource(ChangeType_NewPatient, ResourceType_Patient, hashPatient, status.patientId_); + } + + // Ensure there is enough room in the storage for the new instance uint64_t instanceSize = 0; for (Attachments::const_iterator it = attachments.begin(); @@ -656,208 +799,165 @@ instanceSize += it->GetCompressedSize(); } - Recycle(instanceSize, instanceToStore.GetHasher().HashPatient()); - - // Create the instance - int64_t instance = CreateResource(instanceToStore.GetHasher().HashInstance(), ResourceType_Instance); - ServerToolbox::StoreMainDicomTags(db_, instance, ResourceType_Instance, dicomSummary); - - // Detect up to which level the patient/study/series/instance - // hierarchy must be created - int64_t patient = -1, study = -1, series = -1; - bool isNewPatient = false; - bool isNewStudy = false; - bool isNewSeries = false; - - { - ResourceType dummy; - - if (db_.LookupResource(series, dummy, instanceToStore.GetHasher().HashSeries())) - { - assert(dummy == ResourceType_Series); - // The patient, the study and the series already exist - - bool ok = (db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient()) && - db_.LookupResource(study, dummy, instanceToStore.GetHasher().HashStudy())); - assert(ok); - } - else if (db_.LookupResource(study, dummy, instanceToStore.GetHasher().HashStudy())) - { - assert(dummy == ResourceType_Study); - - // New series: The patient and the study already exist - isNewSeries = true; - - bool ok = db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient()); - assert(ok); - } - else if (db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient())) - { - assert(dummy == ResourceType_Patient); - - // New study and series: The patient already exist - isNewStudy = true; - isNewSeries = true; - } - else - { - // New patient, study and series: Nothing exists - isNewPatient = true; - isNewStudy = true; - isNewSeries = true; - } - } - - // Create the series if needed - if (isNewSeries) - { - series = CreateResource(instanceToStore.GetHasher().HashSeries(), ResourceType_Series); - ServerToolbox::StoreMainDicomTags(db_, series, ResourceType_Series, dicomSummary); - } - - // Create the study if needed - if (isNewStudy) - { - study = CreateResource(instanceToStore.GetHasher().HashStudy(), ResourceType_Study); - ServerToolbox::StoreMainDicomTags(db_, study, ResourceType_Study, dicomSummary); - } - - // Create the patient if needed - if (isNewPatient) - { - patient = CreateResource(instanceToStore.GetHasher().HashPatient(), ResourceType_Patient); - ServerToolbox::StoreMainDicomTags(db_, patient, ResourceType_Patient, dicomSummary); - } - - // Create the parent-to-child links - db_.AttachChild(series, instance); - - if (isNewSeries) - { - db_.AttachChild(study, series); - } - - if (isNewStudy) - { - db_.AttachChild(patient, study); - } - - // Sanity checks - assert(patient != -1); - assert(study != -1); - assert(series != -1); - assert(instance != -1); - + Recycle(instanceSize, hashPatient /* don't consider the current patient for recycling */); + + // Attach the files to the newly created instance for (Attachments::const_iterator it = attachments.begin(); it != attachments.end(); ++it) { - db_.AddAttachment(instance, *it); - } - - // Attach the user-specified metadata - for (MetadataMap::const_iterator - it = metadata.begin(); it != metadata.end(); ++it) - { - switch (it->first.first) - { - case ResourceType_Patient: - db_.SetMetadata(patient, it->first.second, it->second); - break; - - case ResourceType_Study: - db_.SetMetadata(study, it->first.second, it->second); - break; - - case ResourceType_Series: - db_.SetMetadata(series, it->first.second, it->second); - break; - - case ResourceType_Instance: - SetInstanceMetadata(instanceMetadata, instance, it->first.second, it->second); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } + db_.AddAttachment(instanceId, *it); } - // Attach the auto-computed metadata for the patient/study/series levels - std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */); - db_.SetMetadata(series, MetadataType_LastUpdate, now); - db_.SetMetadata(study, MetadataType_LastUpdate, now); - db_.SetMetadata(patient, MetadataType_LastUpdate, now); - - // Attach the auto-computed metadata for the instance level, - // reflecting these additions into the input metadata map - SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_ReceptionDate, now); - SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteAet, - instanceToStore.GetOrigin().GetRemoteAetC()); - SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_Origin, - EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin())); - + { - std::string s; - - if (instanceToStore.LookupTransferSyntax(s)) + ResourcesContent content; + + // Populate the tags of the newly-created resources + + content.AddResource(instanceId, ResourceType_Instance, dicomSummary); + + if (status.isNewSeries_) + { + content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary); + } + + if (status.isNewStudy_) { - // New in Orthanc 1.2.0 - SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_TransferSyntax, s); + content.AddResource(status.studyId_, ResourceType_Study, dicomSummary); + } + + if (status.isNewPatient_) + { + content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary); } - if (instanceToStore.GetOrigin().LookupRemoteIp(s)) + + // Attach the user-specified metadata + + for (MetadataMap::const_iterator + it = metadata.begin(); it != metadata.end(); ++it) { - // New in Orthanc 1.4.0 - SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteIp, s); + switch (it->first.first) + { + case ResourceType_Patient: + content.AddMetadata(status.patientId_, it->first.second, it->second); + break; + + case ResourceType_Study: + content.AddMetadata(status.studyId_, it->first.second, it->second); + break; + + case ResourceType_Series: + content.AddMetadata(status.seriesId_, it->first.second, it->second); + break; + + case ResourceType_Instance: + SetInstanceMetadata(content, instanceMetadata, instanceId, + it->first.second, it->second); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } } - if (instanceToStore.GetOrigin().LookupCalledAet(s)) + + // Attach the auto-computed metadata for the patient/study/series levels + std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */); + content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now); + content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now); + content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now); + + if (status.isNewSeries_ && + hasExpectedInstances) { - // New in Orthanc 1.4.0 - SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_CalledAet, s); - } - - if (instanceToStore.GetOrigin().LookupHttpUsername(s)) - { - // New in Orthanc 1.4.0 - SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_HttpUsername, s); + content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances, + boost::lexical_cast(expectedInstances)); } - } - - const DicomValue* value; - if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && - !value->IsNull() && - !value->IsBinary()) - { - SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_SopClassUid, value->GetContent()); - } - - if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || - (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) - { - if (!value->IsNull() && + + + // Attach the auto-computed metadata for the instance level, + // reflecting these additions into the input metadata map + SetInstanceMetadata(content, instanceMetadata, instanceId, + MetadataType_Instance_ReceptionDate, now); + SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_RemoteAet, + instanceToStore.GetOrigin().GetRemoteAetC()); + SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_Origin, + EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin())); + + + { + std::string s; + + if (instanceToStore.LookupTransferSyntax(s)) + { + // New in Orthanc 1.2.0 + SetInstanceMetadata(content, instanceMetadata, instanceId, + MetadataType_Instance_TransferSyntax, s); + } + + if (instanceToStore.GetOrigin().LookupRemoteIp(s)) + { + // New in Orthanc 1.4.0 + SetInstanceMetadata(content, instanceMetadata, instanceId, + MetadataType_Instance_RemoteIp, s); + } + + if (instanceToStore.GetOrigin().LookupCalledAet(s)) + { + // New in Orthanc 1.4.0 + SetInstanceMetadata(content, instanceMetadata, instanceId, + MetadataType_Instance_CalledAet, s); + } + + if (instanceToStore.GetOrigin().LookupHttpUsername(s)) + { + // New in Orthanc 1.4.0 + SetInstanceMetadata(content, instanceMetadata, instanceId, + MetadataType_Instance_HttpUsername, s); + } + } + + + const DicomValue* value; + if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL && + !value->IsNull() && !value->IsBinary()) { - SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_IndexInSeries, value->GetContent()); + SetInstanceMetadata(content, instanceMetadata, instanceId, + MetadataType_Instance_SopClassUid, value->GetContent()); } + + + if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL || + (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL) + { + if (!value->IsNull() && + !value->IsBinary()) + { + SetInstanceMetadata(content, instanceMetadata, instanceId, + MetadataType_Instance_IndexInSeries, value->GetContent()); + } + } + + + db_.SetResourcesContent(content); } + // Check whether the series of this new instance is now completed - if (isNewSeries) - { - ComputeExpectedNumberOfInstances(db_, series, dicomSummary); - } - - SeriesStatus seriesStatus = GetSeriesStatus(series); + SeriesStatus seriesStatus = GetSeriesStatus(status.seriesId_); if (seriesStatus == SeriesStatus_Complete) { - LogChange(series, ChangeType_CompletedSeries, ResourceType_Series, instanceToStore.GetHasher().HashSeries()); + LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries); } + // Mark the parent resources of this instance as unstable - MarkAsUnstable(series, ResourceType_Series, instanceToStore.GetHasher().HashSeries()); - MarkAsUnstable(study, ResourceType_Study, instanceToStore.GetHasher().HashStudy()); - MarkAsUnstable(patient, ResourceType_Patient, instanceToStore.GetHasher().HashPatient()); + MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries); + MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy); + MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient); t.Commit(instanceSize); @@ -877,8 +977,7 @@ boost::mutex::scoped_lock lock(mutex_); target = Json::objectValue; - uint64_t cs = currentStorageSize_; - assert(cs == db_.GetTotalCompressedSize()); + uint64_t cs = db_.GetTotalCompressedSize(); uint64_t us = db_.GetTotalUncompressedSize(); target["TotalDiskSize"] = boost::lexical_cast(cs); target["TotalUncompressedSize"] = boost::lexical_cast(us); @@ -892,32 +991,30 @@ } - - SeriesStatus ServerIndex::GetSeriesStatus(int64_t id) + + SeriesStatus ServerIndex::GetSeriesStatus(int64_t id, + int64_t expectedNumberOfInstances) { - // Get the expected number of instances in this series (from the metadata) - int64_t expected; - if (!GetMetadataAsInteger(expected, id, MetadataType_Series_ExpectedNumberOfInstances)) - { - return SeriesStatus_Unknown; - } - - // Loop over the instances of this series - std::list children; - db_.GetChildrenInternalId(children, id); + std::list values; + db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries); std::set instances; - for (std::list::const_iterator - it = children.begin(); it != children.end(); ++it) + + for (std::list::const_iterator + it = values.begin(); it != values.end(); ++it) { - // Get the index of this instance in the series int64_t index; - if (!GetMetadataAsInteger(index, *it, MetadataType_Instance_IndexInSeries)) + + try + { + index = boost::lexical_cast(*it); + } + catch (boost::bad_lexical_cast&) { return SeriesStatus_Unknown; } - - if (!(index > 0 && index <= expected)) + + if (!(index > 0 && index <= expectedNumberOfInstances)) { // Out-of-range instance index return SeriesStatus_Inconsistent; @@ -932,7 +1029,7 @@ instances.insert(index); } - if (static_cast(instances.size()) == expected) + if (static_cast(instances.size()) == expectedNumberOfInstances) { return SeriesStatus_Complete; } @@ -943,6 +1040,21 @@ } + SeriesStatus ServerIndex::GetSeriesStatus(int64_t id) + { + // Get the expected number of instances in this series (from the metadata) + int64_t expected; + if (!GetMetadataAsInteger(expected, id, MetadataType_Series_ExpectedNumberOfInstances)) + { + return SeriesStatus_Unknown; + } + else + { + return GetSeriesStatus(id, expected); + } + } + + void ServerIndex::MainDicomTagsToJson(Json::Value& target, int64_t resourceId, ResourceType resourceType) @@ -969,6 +1081,7 @@ } } + bool ServerIndex::LookupResource(Json::Value& result, const std::string& publicId, ResourceType expectedType) @@ -1067,9 +1180,13 @@ int64_t i; if (GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances)) + { result["ExpectedNumberOfInstances"] = static_cast(i); + } else + { result["ExpectedNumberOfInstances"] = Json::nullValue; + } break; } @@ -1089,9 +1206,13 @@ int64_t i; if (GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries)) + { result["IndexInSeries"] = static_cast(i); + } else + { result["IndexInSeries"] = Json::nullValue; + } break; } @@ -1187,7 +1308,9 @@ const std::list& log, const std::string& name, bool done, - int64_t since) + int64_t since, + bool hasLast, + int64_t last) { Json::Value items = Json::arrayValue; for (typename std::list::const_iterator @@ -1202,7 +1325,19 @@ target[name] = items; target["Done"] = done; - int64_t last = (log.empty() ? since : log.back().GetSeq()); + if (!hasLast) + { + // Best-effort guess of the last index in the sequence + if (log.empty()) + { + last = since; + } + else + { + last = log.back().GetSeq(); + } + } + target["Last"] = static_cast(last); } @@ -1213,6 +1348,8 @@ { std::list changes; bool done; + bool hasLast = false; + int64_t last = 0; { boost::mutex::scoped_lock lock(mutex_); @@ -1220,17 +1357,26 @@ // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as // "GetLastChange()" involves calls to "GetPublicId()" Transaction transaction(*this); + db_.GetChanges(changes, done, since, maxResults); + if (changes.empty()) + { + last = db_.GetLastChangeIndex(); + hasLast = true; + } + transaction.Commit(0); } - FormatLog(target, changes, "Changes", done, since); + FormatLog(target, changes, "Changes", done, since, hasLast, last); } void ServerIndex::GetLastChange(Json::Value& target) { std::list changes; + bool hasLast = false; + int64_t last = 0; { boost::mutex::scoped_lock lock(mutex_); @@ -1238,11 +1384,18 @@ // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as // "GetLastChange()" involves calls to "GetPublicId()" Transaction transaction(*this); + db_.GetLastChange(changes); + if (changes.empty()) + { + last = db_.GetLastChangeIndex(); + hasLast = true; + } + transaction.Commit(0); } - FormatLog(target, changes, "Changes", true, 0); + FormatLog(target, changes, "Changes", true, 0, hasLast, last); } @@ -1348,7 +1501,7 @@ db_.GetExportedResources(exported, done, since, maxResults); } - FormatLog(target, exported, "Exports", done, since); + FormatLog(target, exported, "Exports", done, since, false, -1); } @@ -1361,7 +1514,7 @@ db_.GetLastExportedResource(exported); } - FormatLog(target, exported, "Exports", true, 0); + FormatLog(target, exported, "Exports", true, 0, false, -1); } @@ -1369,10 +1522,9 @@ { if (maximumStorageSize_ != 0) { - uint64_t currentSize = currentStorageSize_ - listener_->GetSizeOfFilesToRemove(); - assert(db_.GetTotalCompressedSize() == currentSize); - - if (currentSize + instanceSize > maximumStorageSize_) + assert(maximumStorageSize_ >= instanceSize); + + if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize)) { return true; } @@ -2030,7 +2182,7 @@ - void ServerIndex::LookupIdentifierExact(std::list& result, + void ServerIndex::LookupIdentifierExact(std::vector& result, ResourceType level, const DicomTag& tag, const std::string& value) @@ -2043,11 +2195,19 @@ result.clear(); - boost::mutex::scoped_lock lock(mutex_); - - LookupIdentifierQuery query(level); - query.AddConstraint(tag, IdentifierConstraintType_Equal, value); - query.Apply(result, db_); + DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true); + + std::vector query; + query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + + std::list tmp; + + { + boost::mutex::scoped_lock lock(mutex_); + db_.ApplyLookupResources(tmp, NULL, query, level, 0); + } + + CopyListToVector(result, tmp); } @@ -2337,36 +2497,6 @@ } - void ServerIndex::FindCandidates(std::vector& resources, - std::vector& instances, - const ::Orthanc::LookupResource& lookup) - { - boost::mutex::scoped_lock lock(mutex_); - - std::list tmp; - lookup.FindCandidates(tmp, db_); - - resources.resize(tmp.size()); - instances.resize(tmp.size()); - - size_t pos = 0; - for (std::list::const_iterator - it = tmp.begin(); it != tmp.end(); ++it, pos++) - { - assert(db_.GetResourceType(*it) == lookup.GetLevel()); - - int64_t instance; - if (!ServerToolbox::FindOneChildInstance(instance, db_, *it, lookup.GetLevel())) - { - throw OrthancException(ErrorCode_InternalError); - } - - resources[pos] = db_.GetPublicId(*it); - instances[pos] = db_.GetPublicId(instance); - } - } - - bool ServerIndex::LookupParent(std::string& target, const std::string& publicId, ResourceType parentType) @@ -2432,10 +2562,14 @@ db_.ClearMainDicomTags(series); db_.ClearMainDicomTags(instance); - ServerToolbox::StoreMainDicomTags(db_, patient, ResourceType_Patient, summary); - ServerToolbox::StoreMainDicomTags(db_, study, ResourceType_Study, summary); - ServerToolbox::StoreMainDicomTags(db_, series, ResourceType_Series, summary); - ServerToolbox::StoreMainDicomTags(db_, instance, ResourceType_Instance, summary); + { + ResourcesContent content; + content.AddResource(patient, ResourceType_Patient, summary); + content.AddResource(study, ResourceType_Study, summary); + content.AddResource(series, ResourceType_Series, summary); + content.AddResource(instance, ResourceType_Instance, summary); + db_.SetResourcesContent(content); + } { std::string s; @@ -2460,4 +2594,69 @@ LOG(ERROR) << "EXCEPTION [" << e.What() << "]"; } } + + + void ServerIndex::NormalizeLookup(std::vector& target, + const DatabaseLookup& source, + ResourceType queryLevel) const + { + assert(mainDicomTagsRegistry_.get() != NULL); + + target.clear(); + target.reserve(source.GetConstraintsCount()); + + for (size_t i = 0; i < source.GetConstraintsCount(); i++) + { + ResourceType level; + DicomTagType type; + + mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag()); + + if (type == DicomTagType_Identifier || + type == DicomTagType_Main) + { + // Use the fact that patient-level tags are copied at the study level + if (level == ResourceType_Patient && + queryLevel != ResourceType_Patient) + { + level = ResourceType_Study; + } + + target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type)); + } + } + } + + + void ServerIndex::ApplyLookupResources(std::vector& resourcesId, + std::vector* instancesId, + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t limit) + { + std::vector normalized; + NormalizeLookup(normalized, lookup, queryLevel); + + std::list resourcesList, instancesList; + + { + boost::mutex::scoped_lock lock(mutex_); + + if (instancesId == NULL) + { + db_.ApplyLookupResources(resourcesList, NULL, normalized, queryLevel, limit); + } + else + { + db_.ApplyLookupResources(resourcesList, &instancesList, normalized, queryLevel, limit); + } + } + + CopyListToVector(resourcesId, resourcesList); + + if (instancesId != NULL) + { + CopyListToVector(*instancesId, instancesList); + } + } } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerIndex.h Thu Jan 24 10:55:19 2019 +0100 @@ -33,21 +33,20 @@ #pragma once +#include "../Core/Cache/LeastRecentlyUsedIndex.h" +#include "../Core/DicomFormat/DicomMap.h" + +#include "Database/IDatabaseWrapper.h" + #include #include -#include "../Core/Cache/LeastRecentlyUsedIndex.h" -#include "../Core/SQLite/Connection.h" -#include "../Core/DicomFormat/DicomMap.h" -#include "ServerEnumerations.h" - -#include "IDatabaseWrapper.h" namespace Orthanc { - class LookupResource; - class ServerContext; + class DatabaseLookup; class DicomInstanceToStore; class ParsedDicomFile; + class ServerContext; class ServerIndex : public boost::noncopyable { @@ -59,6 +58,7 @@ class Listener; class Transaction; class UnstableResourcePayload; + class MainDicomTagsRegistry; bool done_; boost::mutex mutex_; @@ -69,10 +69,10 @@ IDatabaseWrapper& db_; LeastRecentlyUsedIndex unstableResources_; - uint64_t currentStorageSize_; uint64_t maximumStorageSize_; unsigned int maximumPatients_; bool overwrite_; + std::auto_ptr mainDicomTagsRegistry_; static void FlushThread(ServerIndex* that, unsigned int threadSleep); @@ -116,15 +116,19 @@ ResourceType resourceType, const std::string& publicId); + void SignalNewResource(ChangeType changeType, + ResourceType level, + const std::string& publicId, + int64_t internalId); + uint64_t IncrementGlobalSequenceInternal(GlobalProperty property); - int64_t CreateResource(const std::string& publicId, - ResourceType type); + void NormalizeLookup(std::vector& target, + const DatabaseLookup& source, + ResourceType level) const; - void SetInstanceMetadata(std::map& instanceMetadata, - int64_t instance, - MetadataType metadata, - const std::string& value); + SeriesStatus GetSeriesStatus(int64_t id, + int64_t expectedNumberOfInstances); public: ServerIndex(ServerContext& context, @@ -250,7 +254,7 @@ /* out */ uint64_t& dicomUncompressedSize, const std::string& publicId); - void LookupIdentifierExact(std::list& result, + void LookupIdentifierExact(std::vector& result, ResourceType level, const DicomTag& tag, const std::string& value); @@ -284,14 +288,16 @@ unsigned int GetDatabaseVersion(); - void FindCandidates(std::vector& resources, - std::vector& instances, - const ::Orthanc::LookupResource& lookup); - bool LookupParent(std::string& target, const std::string& publicId, ResourceType parentType); void ReconstructInstance(ParsedDicomFile& dicom); + + void ApplyLookupResources(std::vector& resourcesId, + std::vector* instancesId, // Can be NULL if not needed + const DatabaseLookup& lookup, + ResourceType queryLevel, + size_t limit); }; } diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/ArchiveJob.cpp --- a/OrthancServer/ServerJobs/ArchiveJob.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/ArchiveJob.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -34,10 +34,12 @@ #include "../PrecompiledHeadersServer.h" #include "ArchiveJob.h" +#include "../../Core/Cache/SharedArchive.h" #include "../../Core/Compression/HierarchicalZipWriter.h" #include "../../Core/DicomParsing/DicomDirWriter.h" #include "../../Core/Logging.h" #include "../../Core/OrthancException.h" +#include "../ServerContext.h" #include diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/ArchiveJob.h --- a/OrthancServer/ServerJobs/ArchiveJob.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/ArchiveJob.h Thu Jan 24 10:55:19 2019 +0100 @@ -35,10 +35,14 @@ #include "../../Core/JobsEngine/IJob.h" #include "../../Core/TemporaryFile.h" -#include "../ServerContext.h" + +#include +#include namespace Orthanc { + class ServerContext; + class ArchiveJob : public IJob { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/DicomModalityStoreJob.cpp --- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -36,6 +36,8 @@ #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" +#include "../ServerContext.h" + namespace Orthanc { diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/DicomModalityStoreJob.h --- a/OrthancServer/ServerJobs/DicomModalityStoreJob.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h Thu Jan 24 10:55:19 2019 +0100 @@ -36,10 +36,10 @@ #include "../../Core/JobsEngine/SetOfInstancesJob.h" #include "../../Core/DicomNetworking/DicomUserConnection.h" -#include "../ServerContext.h" - namespace Orthanc { + class ServerContext; + class DicomModalityStoreJob : public SetOfInstancesJob { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/DicomMoveScuJob.cpp --- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -34,6 +34,7 @@ #include "DicomMoveScuJob.h" #include "../../Core/SerializationToolbox.h" +#include "../ServerContext.h" namespace Orthanc { diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/DicomMoveScuJob.h --- a/OrthancServer/ServerJobs/DicomMoveScuJob.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/DicomMoveScuJob.h Thu Jan 24 10:55:19 2019 +0100 @@ -37,10 +37,11 @@ #include "../../Core/DicomNetworking/DicomUserConnection.h" #include "../QueryRetrieveHandler.h" -#include "../ServerContext.h" namespace Orthanc { + class ServerContext; + class DicomMoveScuJob : public SetOfCommandsJob { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/MergeStudyJob.cpp --- a/OrthancServer/ServerJobs/MergeStudyJob.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -36,6 +36,7 @@ #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" +#include "../ServerContext.h" namespace Orthanc diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/MergeStudyJob.h --- a/OrthancServer/ServerJobs/MergeStudyJob.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/MergeStudyJob.h Thu Jan 24 10:55:19 2019 +0100 @@ -33,12 +33,14 @@ #pragma once +#include "../../Core/DicomFormat/DicomMap.h" #include "../../Core/JobsEngine/SetOfInstancesJob.h" - -#include "../ServerContext.h" +#include "../DicomInstanceOrigin.h" namespace Orthanc { + class ServerContext; + class MergeStudyJob : public SetOfInstancesJob { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp --- a/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -35,6 +35,7 @@ #include "DeleteResourceOperation.h" #include "DicomInstanceOperationValue.h" +#include "../../ServerContext.h" #include "../../../Core/Logging.h" #include "../../../Core/OrthancException.h" diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h --- a/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h Thu Jan 24 10:55:19 2019 +0100 @@ -35,10 +35,10 @@ #include "../../../Core/JobsEngine/Operations/IJobOperation.h" -#include "../../ServerContext.h" - namespace Orthanc { + class ServerContext; + class DeleteResourceOperation : public IJobOperation { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,45 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "DicomInstanceOperationValue.h" + +#include "../../ServerContext.h" + +namespace Orthanc +{ + void DicomInstanceOperationValue::ReadDicom(std::string& dicom) const + { + context_.ReadDicom(dicom, id_); + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h --- a/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h Thu Jan 24 10:55:19 2019 +0100 @@ -35,10 +35,10 @@ #include "../../../Core/JobsEngine/Operations/JobOperationValue.h" -#include "../../ServerContext.h" - namespace Orthanc { + class ServerContext; + class DicomInstanceOperationValue : public JobOperationValue { private: @@ -64,10 +64,7 @@ return id_; } - void ReadDicom(std::string& dicom) const - { - context_.ReadDicom(dicom, id_); - } + void ReadDicom(std::string& dicom) const; virtual JobOperationValue* Clone() const { diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp --- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -35,6 +35,7 @@ #include "ModifyInstanceOperation.h" #include "DicomInstanceOperationValue.h" +#include "../../ServerContext.h" #include "../../../Core/Logging.h" #include "../../../Core/SerializationToolbox.h" diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h --- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h Thu Jan 24 10:55:19 2019 +0100 @@ -36,10 +36,10 @@ #include "../../../Core/JobsEngine/Operations/IJobOperation.h" #include "../../../Core/DicomParsing/DicomModification.h" -#include "../../ServerContext.h" - namespace Orthanc { + class ServerContext; + class ModifyInstanceOperation : public IJobOperation { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp --- a/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -42,6 +42,7 @@ #include "../../../Core/SerializationToolbox.h" #include "../../../Core/TemporaryFile.h" #include "../../../Core/Toolbox.h" +#include "../../../Core/SystemToolbox.h" namespace Orthanc { diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/OrthancJobUnserializer.cpp --- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -37,6 +37,8 @@ #include "../../Core/Logging.h" #include "../../Core/OrthancException.h" #include "../../Core/SerializationToolbox.h" +#include "../../Plugins/Engine/OrthancPlugins.h" +#include "../ServerContext.h" #include "Operations/DeleteResourceOperation.h" #include "Operations/DicomInstanceOperationValue.h" @@ -52,6 +54,7 @@ #include "MergeStudyJob.h" #include "SplitStudyJob.h" + namespace Orthanc { IJob* OrthancJobUnserializer::UnserializeJob(const Json::Value& source) diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/OrthancJobUnserializer.h --- a/OrthancServer/ServerJobs/OrthancJobUnserializer.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.h Thu Jan 24 10:55:19 2019 +0100 @@ -33,11 +33,12 @@ #pragma once -#include "../ServerContext.h" #include "../../Core/JobsEngine/GenericJobUnserializer.h" namespace Orthanc { + class ServerContext; + class OrthancJobUnserializer : public GenericJobUnserializer { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp --- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -35,6 +35,7 @@ #include "OrthancPeerStoreJob.h" #include "../../Core/Logging.h" +#include "../ServerContext.h" namespace Orthanc diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/OrthancPeerStoreJob.h --- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.h Thu Jan 24 10:55:19 2019 +0100 @@ -36,11 +36,11 @@ #include "../../Core/JobsEngine/SetOfInstancesJob.h" #include "../../Core/HttpClient.h" -#include "../ServerContext.h" - namespace Orthanc { + class ServerContext; + class OrthancPeerStoreJob : public SetOfInstancesJob { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/ResourceModificationJob.cpp --- a/OrthancServer/ServerJobs/ResourceModificationJob.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -36,6 +36,7 @@ #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" +#include "../ServerContext.h" namespace Orthanc { diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/ResourceModificationJob.h --- a/OrthancServer/ServerJobs/ResourceModificationJob.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/ResourceModificationJob.h Thu Jan 24 10:55:19 2019 +0100 @@ -34,10 +34,13 @@ #pragma once #include "../../Core/JobsEngine/SetOfInstancesJob.h" -#include "../ServerContext.h" +#include "../../Core/DicomParsing/DicomModification.h" +#include "../DicomInstanceOrigin.h" namespace Orthanc { + class ServerContext; + class ResourceModificationJob : public SetOfInstancesJob { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/SplitStudyJob.cpp --- a/OrthancServer/ServerJobs/SplitStudyJob.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -36,6 +36,8 @@ #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" +#include "../ServerContext.h" + namespace Orthanc { diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerJobs/SplitStudyJob.h --- a/OrthancServer/ServerJobs/SplitStudyJob.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerJobs/SplitStudyJob.h Thu Jan 24 10:55:19 2019 +0100 @@ -34,11 +34,13 @@ #pragma once #include "../../Core/JobsEngine/SetOfInstancesJob.h" - -#include "../ServerContext.h" +#include "../../Core/DicomFormat/DicomTag.h" +#include "../DicomInstanceOrigin.h" namespace Orthanc { + class ServerContext; + class SplitStudyJob : public SetOfInstancesJob { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerToolbox.cpp --- a/OrthancServer/ServerToolbox.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerToolbox.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -35,45 +35,134 @@ #include "ServerToolbox.h" #include "../Core/DicomFormat/DicomArray.h" +#include "../Core/DicomParsing/ParsedDicomFile.h" #include "../Core/FileStorage/StorageAccessor.h" #include "../Core/Logging.h" #include "../Core/OrthancException.h" +#include "Database/IDatabaseWrapper.h" +#include "Database/ResourcesContent.h" +#include "ServerContext.h" #include namespace Orthanc { + static const DicomTag PATIENT_IDENTIFIERS[] = + { + DICOM_TAG_PATIENT_ID, + DICOM_TAG_PATIENT_NAME, + DICOM_TAG_PATIENT_BIRTH_DATE + }; + + static const DicomTag STUDY_IDENTIFIERS[] = + { + DICOM_TAG_PATIENT_ID, + DICOM_TAG_PATIENT_NAME, + DICOM_TAG_PATIENT_BIRTH_DATE, + DICOM_TAG_STUDY_INSTANCE_UID, + DICOM_TAG_ACCESSION_NUMBER, + DICOM_TAG_STUDY_DESCRIPTION, + DICOM_TAG_STUDY_DATE + }; + + static const DicomTag SERIES_IDENTIFIERS[] = + { + DICOM_TAG_SERIES_INSTANCE_UID + }; + + static const DicomTag INSTANCE_IDENTIFIERS[] = + { + DICOM_TAG_SOP_INSTANCE_UID + }; + + + static void StoreMainDicomTagsInternal(ResourcesContent& target, + int64_t resource, + const DicomMap& tags) + { + DicomArray flattened(tags); + + for (size_t i = 0; i < flattened.GetSize(); i++) + { + const DicomElement& element = flattened.GetElement(i); + const DicomTag& tag = element.GetTag(); + const DicomValue& value = element.GetValue(); + if (!value.IsNull() && + !value.IsBinary()) + { + target.AddMainDicomTag(resource, tag, element.GetValue().GetContent()); + } + } + } + + + static void StoreIdentifiers(ResourcesContent& target, + int64_t resource, + ResourceType level, + const DicomMap& map) + { + const DicomTag* tags; + size_t size; + + ServerToolbox::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + // The identifiers tags are a subset of the main DICOM tags + assert(DicomMap::IsMainDicomTag(tags[i])); + + const DicomValue* value = map.TestAndGetValue(tags[i]); + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + std::string s = ServerToolbox::NormalizeIdentifier(value->GetContent()); + target.AddIdentifierTag(resource, tags[i], s); + } + } + } + + + void ResourcesContent::AddResource(int64_t resource, + ResourceType level, + const DicomMap& dicomSummary) + { + StoreIdentifiers(*this, resource, level, dicomSummary); + + DicomMap tags; + + switch (level) + { + case ResourceType_Patient: + dicomSummary.ExtractPatientInformation(tags); + break; + + case ResourceType_Study: + // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6) + dicomSummary.ExtractPatientInformation(tags); + StoreMainDicomTagsInternal(*this, resource, tags); + + dicomSummary.ExtractStudyInformation(tags); + break; + + case ResourceType_Series: + dicomSummary.ExtractSeriesInformation(tags); + break; + + case ResourceType_Instance: + dicomSummary.ExtractInstanceInformation(tags); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + StoreMainDicomTagsInternal(*this, resource, tags); + } + + namespace ServerToolbox { - static const DicomTag patientIdentifiers[] = - { - DICOM_TAG_PATIENT_ID, - DICOM_TAG_PATIENT_NAME, - DICOM_TAG_PATIENT_BIRTH_DATE - }; - - static const DicomTag studyIdentifiers[] = - { - DICOM_TAG_PATIENT_ID, - DICOM_TAG_PATIENT_NAME, - DICOM_TAG_PATIENT_BIRTH_DATE, - DICOM_TAG_STUDY_INSTANCE_UID, - DICOM_TAG_ACCESSION_NUMBER, - DICOM_TAG_STUDY_DESCRIPTION, - DICOM_TAG_STUDY_DATE - }; - - static const DicomTag seriesIdentifiers[] = - { - DICOM_TAG_SERIES_INSTANCE_UID - }; - - static const DicomTag instanceIdentifiers[] = - { - DICOM_TAG_SOP_INSTANCE_UID - }; - - void SimplifyTags(Json::Value& target, const Json::Value& source, DicomToJsonFormat format) @@ -138,91 +227,6 @@ } - static void StoreMainDicomTagsInternal(IDatabaseWrapper& database, - int64_t resource, - const DicomMap& tags) - { - DicomArray flattened(tags); - - for (size_t i = 0; i < flattened.GetSize(); i++) - { - const DicomElement& element = flattened.GetElement(i); - const DicomTag& tag = element.GetTag(); - const DicomValue& value = element.GetValue(); - if (!value.IsNull() && - !value.IsBinary()) - { - database.SetMainDicomTag(resource, tag, element.GetValue().GetContent()); - } - } - } - - - static void StoreIdentifiers(IDatabaseWrapper& database, - int64_t resource, - ResourceType level, - const DicomMap& map) - { - const DicomTag* tags; - size_t size; - - LoadIdentifiers(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - const DicomValue* value = map.TestAndGetValue(tags[i]); - if (value != NULL && - !value->IsNull() && - !value->IsBinary()) - { - std::string s = NormalizeIdentifier(value->GetContent()); - database.SetIdentifierTag(resource, tags[i], s); - } - } - } - - - void StoreMainDicomTags(IDatabaseWrapper& database, - int64_t resource, - ResourceType level, - const DicomMap& dicomSummary) - { - // WARNING: The database should be locked with a transaction! - - StoreIdentifiers(database, resource, level, dicomSummary); - - DicomMap tags; - - switch (level) - { - case ResourceType_Patient: - dicomSummary.ExtractPatientInformation(tags); - break; - - case ResourceType_Study: - // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6) - dicomSummary.ExtractPatientInformation(tags); - StoreMainDicomTagsInternal(database, resource, tags); - - dicomSummary.ExtractStudyInformation(tags); - break; - - case ResourceType_Series: - dicomSummary.ExtractSeriesInformation(tags); - break; - - case ResourceType_Instance: - dicomSummary.ExtractInstanceInformation(tags); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - StoreMainDicomTagsInternal(database, resource, tags); - } - - bool FindOneChildInstance(int64_t& result, IDatabaseWrapper& database, int64_t resource, @@ -332,7 +336,10 @@ dicom.ExtractDicomSummary(dicomSummary); database.ClearMainDicomTags(resource); - StoreMainDicomTags(database, resource, level, dicomSummary); + + ResourcesContent tags; + tags.AddResource(resource, level, dicomSummary); + database.SetResourcesContent(tags); } catch (OrthancException&) { @@ -351,23 +358,23 @@ switch (level) { case ResourceType_Patient: - tags = patientIdentifiers; - size = sizeof(patientIdentifiers) / sizeof(DicomTag); + tags = PATIENT_IDENTIFIERS; + size = sizeof(PATIENT_IDENTIFIERS) / sizeof(DicomTag); break; case ResourceType_Study: - tags = studyIdentifiers; - size = sizeof(studyIdentifiers) / sizeof(DicomTag); + tags = STUDY_IDENTIFIERS; + size = sizeof(STUDY_IDENTIFIERS) / sizeof(DicomTag); break; case ResourceType_Series: - tags = seriesIdentifiers; - size = sizeof(seriesIdentifiers) / sizeof(DicomTag); + tags = SERIES_IDENTIFIERS; + size = sizeof(SERIES_IDENTIFIERS) / sizeof(DicomTag); break; case ResourceType_Instance: - tags = instanceIdentifiers; - size = sizeof(instanceIdentifiers) / sizeof(DicomTag); + tags = INSTANCE_IDENTIFIERS; + size = sizeof(INSTANCE_IDENTIFIERS) / sizeof(DicomTag); break; default: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/ServerToolbox.h --- a/OrthancServer/ServerToolbox.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/ServerToolbox.h Thu Jan 24 10:55:19 2019 +0100 @@ -33,23 +33,24 @@ #pragma once -#include "ServerContext.h" +#include "ServerEnumerations.h" #include +#include +#include namespace Orthanc { + class ServerContext; + class IDatabaseWrapper; + class IStorageArea; + namespace ServerToolbox { void SimplifyTags(Json::Value& target, const Json::Value& source, DicomToJsonFormat format); - void StoreMainDicomTags(IDatabaseWrapper& database, - int64_t resource, - ResourceType level, - const DicomMap& dicomSummary); - bool FindOneChildInstance(int64_t& result, IDatabaseWrapper& database, int64_t resource, diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/SliceOrdering.cpp --- a/OrthancServer/SliceOrdering.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/SliceOrdering.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -37,6 +37,7 @@ #include "../Core/Logging.h" #include "../Core/Toolbox.h" #include "ServerEnumerations.h" +#include "ServerIndex.h" #include #include diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/SliceOrdering.h --- a/OrthancServer/SliceOrdering.h Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/SliceOrdering.h Thu Jan 24 10:55:19 2019 +0100 @@ -33,10 +33,12 @@ #pragma once -#include "ServerIndex.h" +#include "../Core/DicomFormat/DicomMap.h" namespace Orthanc { + class ServerIndex; + class SliceOrdering { private: diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Upgrade3To4.sql --- a/OrthancServer/Upgrade3To4.sql Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,24 +0,0 @@ --- This SQLite script updates the version of the Orthanc database from 3 to 4. - --- Add 2 new columns at "AttachedFiles" - -ALTER TABLE AttachedFiles ADD COLUMN uncompressedMD5 TEXT; -ALTER TABLE AttachedFiles ADD COLUMN compressedMD5 TEXT; - --- Update the "AttachedFileDeleted" trigger - -DROP TRIGGER AttachedFileDeleted; - -CREATE TRIGGER AttachedFileDeleted -AFTER DELETE ON AttachedFiles -BEGIN - SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, - old.compressionType, old.compressedSize, - -- These 2 arguments are new in Orthanc 0.7.3 (database v4) - old.uncompressedMD5, old.compressedMD5); -END; - --- Change the database version --- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration - -UPDATE GlobalProperties SET value="4" WHERE property=1; diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/Upgrade4To5.sql --- a/OrthancServer/Upgrade4To5.sql Thu Jan 24 10:54:47 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,66 +0,0 @@ --- This SQLite script updates the version of the Orthanc database from 4 to 5. - - --- Remove 2 indexes to speed up - -DROP INDEX MainDicomTagsIndex2; -DROP INDEX MainDicomTagsIndexValues; - - --- Add a new table to index the DICOM identifiers - -CREATE TABLE DicomIdentifiers( - id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE, - tagGroup INTEGER, - tagElement INTEGER, - value TEXT, - PRIMARY KEY(id, tagGroup, tagElement) - ); - -CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id); -CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement); -CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY); - - --- Migrate data from MainDicomTags to MainResourcesTags and MainInstancesTags - -INSERT INTO DicomIdentifiers SELECT * FROM MainDicomTags - WHERE ((tagGroup = 16 AND tagElement = 32) OR -- PatientID (0x0010, 0x0020) - (tagGroup = 32 AND tagElement = 13) OR -- StudyInstanceUID (0x0020, 0x000d) - (tagGroup = 8 AND tagElement = 80) OR -- AccessionNumber (0x0008, 0x0050) - (tagGroup = 32 AND tagElement = 14) OR -- SeriesInstanceUID (0x0020, 0x000e) - (tagGroup = 8 AND tagElement = 24)); -- SOPInstanceUID (0x0008, 0x0018) - -DELETE FROM MainDicomTags - WHERE ((tagGroup = 16 AND tagElement = 32) OR -- PatientID (0x0010, 0x0020) - (tagGroup = 32 AND tagElement = 13) OR -- StudyInstanceUID (0x0020, 0x000d) - (tagGroup = 8 AND tagElement = 80) OR -- AccessionNumber (0x0008, 0x0050) - (tagGroup = 32 AND tagElement = 14) OR -- SeriesInstanceUID (0x0020, 0x000e) - (tagGroup = 8 AND tagElement = 24)); -- SOPInstanceUID (0x0008, 0x0018) - - --- Upgrade the "ResourceDeleted" trigger - -DROP TRIGGER ResourceDeleted; -DROP TRIGGER ResourceDeletedParentCleaning; - -CREATE TRIGGER ResourceDeleted -AFTER DELETE ON Resources -BEGIN - SELECT SignalResourceDeleted(old.publicId, old.resourceType); - SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) - FROM Resources AS parent WHERE internalId = old.parentId; -END; - -CREATE TRIGGER ResourceDeletedParentCleaning -AFTER DELETE ON Resources -FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0 -BEGIN - DELETE FROM Resources WHERE internalId = old.parentId; -END; - - --- Change the database version --- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration - -UPDATE GlobalProperties SET value="5" WHERE property=1; diff -r 4cfed5c2eacd -r fc9a4a2dad63 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/OrthancServer/main.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -41,7 +41,7 @@ #include "../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/HttpServer/EmbeddedResourceHttpHandler.h" #include "../Core/HttpServer/FilesystemHttpHandler.h" -#include "../Core/HttpServer/MongooseServer.h" +#include "../Core/HttpServer/HttpServer.h" #include "../Core/Logging.h" #include "../Core/Lua/LuaFunctionCall.h" #include "../Plugins/Engine/OrthancPlugins.h" @@ -797,9 +797,17 @@ else { MyIncomingHttpRequestFilter httpFilter(context, plugins); - MongooseServer httpServer; + HttpServer httpServer; bool httpDescribeErrors; +#if ORTHANC_ENABLE_MONGOOSE == 1 + const bool defaultKeepAlive = false; +#elif ORTHANC_ENABLE_CIVETWEB == 1 + const bool defaultKeepAlive = true; +#else +# error "Either Mongoose or Civetweb must be enabled to compile this file" +#endif + { OrthancConfiguration::ReaderLock lock; @@ -809,9 +817,10 @@ //httpServer.SetThreadsCount(50); httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042)); httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false)); - httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", false)); + httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive)); httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", true)); httpServer.SetAuthenticationEnabled(lock.GetConfiguration().GetBooleanParameter("AuthenticationEnabled", false)); + httpServer.SetTcpNoDelay(lock.GetConfiguration().GetBooleanParameter("TcpNoDelay", true)); lock.GetConfiguration().SetupRegisteredUsers(httpServer); @@ -1021,7 +1030,7 @@ catch (OrthancException&) { LOG(ERROR) << "Unable to run the automated upgrade, please use the replication instructions: " - << "http://book.orthanc-server.com//users/replication.html"; + << "http://book.orthanc-server.com/users/replication.html"; throw; } @@ -1176,7 +1185,7 @@ else if (currentVersion != ORTHANC_DATABASE_VERSION) { throw OrthancException(ErrorCode_IncompatibleDatabaseVersion, - "The database schema must be changed from version " + + "The database schema must be upgraded from version " + boost::lexical_cast(currentVersion) + " to " + boost::lexical_cast(ORTHANC_DATABASE_VERSION) + ": Please run Orthanc with the \"--upgrade\" argument"); diff -r 4cfed5c2eacd -r fc9a4a2dad63 Plugins/Engine/OrthancPluginDatabase.cpp --- a/Plugins/Engine/OrthancPluginDatabase.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Plugins/Engine/OrthancPluginDatabase.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -39,14 +39,70 @@ #endif +#include "../../Core/Logging.h" #include "../../Core/OrthancException.h" -#include "../../Core/Logging.h" #include "PluginsEnumerations.h" #include namespace Orthanc { + class OrthancPluginDatabase::Transaction : public IDatabaseWrapper::ITransaction + { + private: + OrthancPluginDatabase& that_; + + void CheckSuccess(OrthancPluginErrorCode code) const + { + if (code != OrthancPluginErrorCode_Success) + { + that_.errorDictionary_.LogError(code, true); + throw OrthancException(static_cast(code)); + } + } + + public: + Transaction(OrthancPluginDatabase& that) : + that_(that) + { + } + + virtual void Begin() + { + CheckSuccess(that_.backend_.startTransaction(that_.payload_)); + } + + virtual void Rollback() + { + CheckSuccess(that_.backend_.rollbackTransaction(that_.payload_)); + } + + virtual void Commit(int64_t diskSizeDelta) + { + if (that_.fastGetTotalSize_) + { + CheckSuccess(that_.backend_.commitTransaction(that_.payload_)); + } + else + { + if (static_cast(that_.currentDiskSize_) + diskSizeDelta < 0) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + uint64_t newDiskSize = (that_.currentDiskSize_ + diskSizeDelta); + + assert(newDiskSize == that_.GetTotalCompressedSize()); + + CheckSuccess(that_.backend_.commitTransaction(that_.payload_)); + + // The transaction has succeeded, we can commit the new disk size + that_.currentDiskSize_ = newDiskSize; + } + } + }; + + static FileInfo Convert(const OrthancPluginAttachment& attachment) { return FileInfo(attachment.uuid, @@ -77,6 +133,8 @@ answerChanges_ = NULL; answerExportedResources_ = NULL; answerDone_ = NULL; + answerMatchingResources_ = NULL; + answerMatchingInstances_ = NULL; } @@ -168,15 +226,14 @@ void *payload) : library_(library), errorDictionary_(errorDictionary), - type_(_OrthancPluginDatabaseAnswerType_None), backend_(backend), payload_(payload), - listener_(NULL), - answerDicomMap_(NULL), - answerChanges_(NULL), - answerExportedResources_(NULL), - answerDone_(NULL) + listener_(NULL) { + static const char* const MISSING = " Missing extension in database index plugin: "; + + ResetAnswers(); + memset(&extensions_, 0, sizeof(extensions_)); size_t size = sizeof(extensions_); @@ -186,6 +243,83 @@ } memcpy(&extensions_, extensions, size); + + bool isOptimal = true; + + if (extensions_.lookupResources == NULL) + { + LOG(INFO) << MISSING << "LookupIdentifierRange()"; + isOptimal = false; + } + + if (extensions_.createInstance == NULL) + { + LOG(INFO) << MISSING << "CreateInstance()"; + isOptimal = false; + } + + if (extensions_.setResourcesContent == NULL) + { + LOG(INFO) << MISSING << "SetResourcesContent()"; + isOptimal = false; + } + + if (extensions_.getChildrenMetadata == NULL) + { + LOG(INFO) << MISSING << "GetChildrenMetadata()"; + isOptimal = false; + } + + if (isOptimal) + { + LOG(INFO) << "The performance of the database index plugin " + << "is optimal for this version of Orthanc"; + } + else + { + LOG(WARNING) << "Performance warning in the database index: " + << "Some extensions are missing in the plugin"; + } + + if (extensions_.getLastChangeIndex == NULL) + { + LOG(WARNING) << "The database extension GetLastChangeIndex() is missing"; + } + + if (extensions_.tagMostRecentPatient == NULL) + { + LOG(WARNING) << "The database extension TagMostRecentPatient() is missing " + << "(affected by issue 58)"; + } + } + + + void OrthancPluginDatabase::Open() + { + CheckSuccess(backend_.open(payload_)); + + { + Transaction transaction(*this); + transaction.Begin(); + + std::string tmp; + fastGetTotalSize_ = + (LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) && + tmp == "1"); + + if (fastGetTotalSize_) + { + currentDiskSize_ = 0; // Unused + } + else + { + // This is the case of database plugins using Orthanc SDK <= 1.5.2 + LOG(WARNING) << "Your database index plugin is not compatible with multiple Orthanc writers"; + currentDiskSize_ = GetTotalCompressedSize(); + } + + transaction.Commit(0); + } } @@ -281,7 +415,7 @@ if (extensions_.getAllInternalIds == NULL) { throw OrthancException(ErrorCode_DatabasePlugin, - "The database plugin does not implement the GetAllInternalIds primitive"); + "The database plugin does not implement the mandatory GetAllInternalIds() extension"); } ResetAnswers(); @@ -610,58 +744,6 @@ } - void OrthancPluginDatabase::LookupIdentifier(std::list& result, - ResourceType level, - const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value) - { - if (extensions_.lookupIdentifier3 == NULL) - { - throw OrthancException(ErrorCode_DatabasePlugin, - "The database plugin does not implement the LookupIdentifier3 primitive"); - } - - OrthancPluginDicomTag tmp; - tmp.group = tag.GetGroup(); - tmp.element = tag.GetElement(); - tmp.value = value.c_str(); - - ResetAnswers(); - CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level), - &tmp, Plugins::Convert(type))); - ForwardAnswers(result); - } - - - void OrthancPluginDatabase::LookupIdentifierRange(std::list& result, - ResourceType level, - const DicomTag& tag, - const std::string& start, - const std::string& end) - { - if (extensions_.lookupIdentifierRange == NULL) - { - // Default implementation, for plugins using Orthanc SDK <= 1.3.2 - - LookupIdentifier(result, level, tag, IdentifierConstraintType_GreaterOrEqual, start); - - std::list b; - LookupIdentifier(result, level, tag, IdentifierConstraintType_SmallerOrEqual, end); - - result.splice(result.end(), b); - } - else - { - ResetAnswers(); - CheckSuccess(extensions_.lookupIdentifierRange(GetContext(), payload_, Plugins::Convert(level), - tag.GetGroup(), tag.GetElement(), - start.c_str(), end.c_str())); - ForwardAnswers(result); - } - } - - bool OrthancPluginDatabase::LookupMetadata(std::string& target, int64_t id, MetadataType type) @@ -737,7 +819,7 @@ if (extensions_.clearMainDicomTags == NULL) { throw OrthancException(ErrorCode_DatabasePlugin, - "Your custom index plugin does not implement the ClearMainDicomTags() extension"); + "Your custom index plugin does not implement the mandatory ClearMainDicomTags() extension"); } CheckSuccess(extensions_.clearMainDicomTags(payload_, id)); @@ -786,52 +868,9 @@ } - class OrthancPluginDatabase::Transaction : public SQLite::ITransaction + IDatabaseWrapper::ITransaction* OrthancPluginDatabase::StartTransaction() { - private: - const OrthancPluginDatabaseBackend& backend_; - void* payload_; - PluginsErrorDictionary& errorDictionary_; - - void CheckSuccess(OrthancPluginErrorCode code) - { - if (code != OrthancPluginErrorCode_Success) - { - errorDictionary_.LogError(code, true); - throw OrthancException(static_cast(code)); - } - } - - public: - Transaction(const OrthancPluginDatabaseBackend& backend, - void* payload, - PluginsErrorDictionary& errorDictionary) : - backend_(backend), - payload_(payload), - errorDictionary_(errorDictionary) - { - } - - virtual void Begin() - { - CheckSuccess(backend_.startTransaction(payload_)); - } - - virtual void Rollback() - { - CheckSuccess(backend_.rollbackTransaction(payload_)); - } - - virtual void Commit() - { - CheckSuccess(backend_.commitTransaction(payload_)); - } - }; - - - SQLite::ITransaction* OrthancPluginDatabase::StartTransaction() - { - return new Transaction(backend_, payload_, errorDictionary_); + return new Transaction(*this); } @@ -892,7 +931,7 @@ { if (extensions_.upgradeDatabase != NULL) { - Transaction transaction(backend_, payload_, errorDictionary_); + Transaction transaction(*this); transaction.Begin(); OrthancPluginErrorCode code = extensions_.upgradeDatabase( @@ -901,7 +940,7 @@ if (code == OrthancPluginErrorCode_Success) { - transaction.Commit(); + transaction.Commit(0); } else { @@ -970,6 +1009,17 @@ answerExportedResources_->clear(); break; + case _OrthancPluginDatabaseAnswerType_MatchingResource: + assert(answerMatchingResources_ != NULL); + answerMatchingResources_->clear(); + + if (answerMatchingInstances_ != NULL) + { + answerMatchingInstances_->clear(); + } + + break; + default: throw OrthancException(ErrorCode_DatabasePlugin, "Unhandled type of answer for custom index plugin: " + @@ -1054,7 +1104,8 @@ } else { - const OrthancPluginChange& change = *reinterpret_cast(answer.valueGeneric); + const OrthancPluginChange& change = + *reinterpret_cast(answer.valueGeneric); assert(answerChanges_ != NULL); answerChanges_->push_back (ServerIndexChange(change.seq, @@ -1098,10 +1149,286 @@ break; } + case _OrthancPluginDatabaseAnswerType_MatchingResource: + { + const OrthancPluginMatchingResource& match = + *reinterpret_cast(answer.valueGeneric); + + if (match.resourceId == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + assert(answerMatchingResources_ != NULL); + answerMatchingResources_->push_back(match.resourceId); + + if (answerMatchingInstances_ != NULL) + { + if (match.someInstanceId == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + answerMatchingInstances_->push_back(match.someInstanceId); + } + + break; + } + default: throw OrthancException(ErrorCode_DatabasePlugin, "Unhandled type of answer for custom index plugin: " + boost::lexical_cast(answer.type)); } } + + + bool OrthancPluginDatabase::IsDiskSizeAbove(uint64_t threshold) + { + if (fastGetTotalSize_) + { + return GetTotalCompressedSize() > threshold; + } + else + { + assert(GetTotalCompressedSize() == currentDiskSize_); + return currentDiskSize_ > threshold; + } + } + + + void OrthancPluginDatabase::ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) + { + if (extensions_.lookupResources == NULL) + { + // Fallback to compatibility mode + ILookupResources::Apply + (*this, *this, resourcesId, instancesId, lookup, queryLevel, limit); + } + else + { + std::vector constraints; + std::vector< std::vector > constraintsValues; + + constraints.resize(lookup.size()); + constraintsValues.resize(lookup.size()); + + for (size_t i = 0; i < lookup.size(); i++) + { + lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]); + } + + ResetAnswers(); + answerMatchingResources_ = &resourcesId; + answerMatchingInstances_ = instancesId; + + CheckSuccess(extensions_.lookupResources(GetContext(), payload_, lookup.size(), + (lookup.empty() ? NULL : &constraints[0]), + Plugins::Convert(queryLevel), + limit, (instancesId == NULL ? 0 : 1))); + } + } + + + bool OrthancPluginDatabase::CreateInstance( + IDatabaseWrapper::CreateInstanceResult& result, + int64_t& instanceId, + const std::string& patient, + const std::string& study, + const std::string& series, + const std::string& instance) + { + if (extensions_.createInstance == NULL) + { + // Fallback to compatibility mode + return ICreateInstance::Apply + (*this, result, instanceId, patient, study, series, instance); + } + else + { + OrthancPluginCreateInstanceResult output; + memset(&output, 0, sizeof(output)); + + CheckSuccess(extensions_.createInstance(&output, payload_, patient.c_str(), + study.c_str(), series.c_str(), instance.c_str())); + + instanceId = output.instanceId; + + if (output.isNewInstance) + { + result.isNewPatient_ = output.isNewPatient; + result.isNewStudy_ = output.isNewStudy; + result.isNewSeries_ = output.isNewSeries; + result.patientId_ = output.patientId; + result.studyId_ = output.studyId; + result.seriesId_ = output.seriesId; + return true; + } + else + { + return false; + } + } + } + + + void OrthancPluginDatabase::LookupIdentifier(std::list& result, + ResourceType level, + const DicomTag& tag, + Compatibility::IdentifierConstraintType type, + const std::string& value) + { + if (extensions_.lookupIdentifier3 == NULL) + { + throw OrthancException(ErrorCode_DatabasePlugin, + "The database plugin does not implement the mandatory LookupIdentifier3() extension"); + } + + OrthancPluginDicomTag tmp; + tmp.group = tag.GetGroup(); + tmp.element = tag.GetElement(); + tmp.value = value.c_str(); + + ResetAnswers(); + CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level), + &tmp, Compatibility::Convert(type))); + ForwardAnswers(result); + } + + + void OrthancPluginDatabase::LookupIdentifierRange(std::list& result, + ResourceType level, + const DicomTag& tag, + const std::string& start, + const std::string& end) + { + if (extensions_.lookupIdentifierRange == NULL) + { + // Default implementation, for plugins using Orthanc SDK <= 1.3.2 + + LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_GreaterOrEqual, start); + + std::list b; + LookupIdentifier(result, level, tag, Compatibility::IdentifierConstraintType_SmallerOrEqual, end); + + result.splice(result.end(), b); + } + else + { + ResetAnswers(); + CheckSuccess(extensions_.lookupIdentifierRange(GetContext(), payload_, Plugins::Convert(level), + tag.GetGroup(), tag.GetElement(), + start.c_str(), end.c_str())); + ForwardAnswers(result); + } + } + + + void OrthancPluginDatabase::SetResourcesContent(const Orthanc::ResourcesContent& content) + { + if (extensions_.setResourcesContent == NULL) + { + ISetResourcesContent::Apply(*this, content); + } + else + { + std::vector identifierTags; + std::vector mainDicomTags; + std::vector metadata; + + identifierTags.reserve(content.GetListTags().size()); + mainDicomTags.reserve(content.GetListTags().size()); + metadata.reserve(content.GetListMetadata().size()); + + for (ResourcesContent::ListTags::const_iterator + it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it) + { + OrthancPluginResourcesContentTags tmp; + tmp.resource = it->resourceId_; + tmp.group = it->tag_.GetGroup(); + tmp.element = it->tag_.GetElement(); + tmp.value = it->value_.c_str(); + + if (it->isIdentifier_) + { + identifierTags.push_back(tmp); + } + else + { + mainDicomTags.push_back(tmp); + } + } + + for (ResourcesContent::ListMetadata::const_iterator + it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it) + { + OrthancPluginResourcesContentMetadata tmp; + tmp.resource = it->resourceId_; + tmp.metadata = it->metadata_; + tmp.value = it->value_.c_str(); + metadata.push_back(tmp); + } + + assert(identifierTags.size() + mainDicomTags.size() == content.GetListTags().size() && + metadata.size() == content.GetListMetadata().size()); + + CheckSuccess(extensions_.setResourcesContent( + payload_, + identifierTags.size(), + (identifierTags.empty() ? NULL : &identifierTags[0]), + mainDicomTags.size(), + (mainDicomTags.empty() ? NULL : &mainDicomTags[0]), + metadata.size(), + (metadata.empty() ? NULL : &metadata[0]))); + } + } + + + + void OrthancPluginDatabase::GetChildrenMetadata(std::list& target, + int64_t resourceId, + MetadataType metadata) + { + if (extensions_.getChildrenMetadata == NULL) + { + IGetChildrenMetadata::Apply(*this, target, resourceId, metadata); + } + else + { + ResetAnswers(); + CheckSuccess(extensions_.getChildrenMetadata + (GetContext(), payload_, resourceId, static_cast(metadata))); + ForwardAnswers(target); + } + } + + + int64_t OrthancPluginDatabase::GetLastChangeIndex() + { + if (extensions_.getLastChangeIndex == NULL) + { + // This was the default behavior in Orthanc <= 1.5.1 + // https://groups.google.com/d/msg/orthanc-users/QhzB6vxYeZ0/YxabgqpfBAAJ + return 0; + } + else + { + int64_t result = 0; + CheckSuccess(extensions_.getLastChangeIndex(&result, payload_)); + return result; + } + } + + + void OrthancPluginDatabase::TagMostRecentPatient(int64_t patient) + { + if (extensions_.tagMostRecentPatient != NULL) + { + CheckSuccess(extensions_.tagMostRecentPatient(payload_, patient)); + } + } } diff -r 4cfed5c2eacd -r fc9a4a2dad63 Plugins/Engine/OrthancPluginDatabase.h --- a/Plugins/Engine/OrthancPluginDatabase.h Thu Jan 24 10:54:47 2019 +0100 +++ b/Plugins/Engine/OrthancPluginDatabase.h Thu Jan 24 10:55:19 2019 +0100 @@ -36,13 +36,21 @@ #if ORTHANC_ENABLE_PLUGINS == 1 #include "../../Core/SharedLibrary.h" -#include "../../OrthancServer/IDatabaseWrapper.h" +#include "../../OrthancServer/Database/Compatibility/ICreateInstance.h" +#include "../../OrthancServer/Database/Compatibility/IGetChildrenMetadata.h" +#include "../../OrthancServer/Database/Compatibility/ILookupResources.h" +#include "../../OrthancServer/Database/Compatibility/ISetResourcesContent.h" #include "../Include/orthanc/OrthancCDatabasePlugin.h" #include "PluginsErrorDictionary.h" namespace Orthanc { - class OrthancPluginDatabase : public IDatabaseWrapper + class OrthancPluginDatabase : + public IDatabaseWrapper, + public Compatibility::ICreateInstance, + public Compatibility::IGetChildrenMetadata, + public Compatibility::ILookupResources, + public Compatibility::ISetResourcesContent { private: class Transaction; @@ -57,6 +65,9 @@ void* payload_; IDatabaseListener* listener_; + bool fastGetTotalSize_; + uint64_t currentDiskSize_; + std::list answerStrings_; std::list answerInt32_; std::list answerInt64_; @@ -67,6 +78,8 @@ std::list* answerChanges_; std::list* answerExportedResources_; bool* answerDone_; + std::list* answerMatchingResources_; + std::list* answerMatchingInstances_; OrthancPluginDatabaseContext* GetContext() { @@ -93,12 +106,11 @@ size_t extensionsSize, void *payload); - virtual void Open() - { - CheckSuccess(backend_.open(payload_)); - } + virtual void Open() + ORTHANC_OVERRIDE; - virtual void Close() + virtual void Close() + ORTHANC_OVERRIDE { CheckSuccess(backend_.close(payload_)); } @@ -109,165 +121,249 @@ } virtual void AddAttachment(int64_t id, - const FileInfo& attachment); + const FileInfo& attachment) + ORTHANC_OVERRIDE; virtual void AttachChild(int64_t parent, - int64_t child); + int64_t child) + ORTHANC_OVERRIDE; - virtual void ClearChanges(); + virtual void ClearChanges() + ORTHANC_OVERRIDE; - virtual void ClearExportedResources(); + virtual void ClearExportedResources() + ORTHANC_OVERRIDE; virtual int64_t CreateResource(const std::string& publicId, - ResourceType type); + ResourceType type) + ORTHANC_OVERRIDE; virtual void DeleteAttachment(int64_t id, - FileContentType attachment); + FileContentType attachment) + ORTHANC_OVERRIDE; virtual void DeleteMetadata(int64_t id, - MetadataType type); + MetadataType type) + ORTHANC_OVERRIDE; - virtual void DeleteResource(int64_t id); + virtual void DeleteResource(int64_t id) + ORTHANC_OVERRIDE; - virtual void FlushToDisk() + virtual void FlushToDisk() + ORTHANC_OVERRIDE { } - virtual bool HasFlushToDisk() const + virtual bool HasFlushToDisk() const + ORTHANC_OVERRIDE { return false; } virtual void GetAllMetadata(std::map& target, - int64_t id); - - virtual void GetAllInternalIds(std::list& target, - ResourceType resourceType); + int64_t id) + ORTHANC_OVERRIDE; virtual void GetAllPublicIds(std::list& target, - ResourceType resourceType); + ResourceType resourceType) + ORTHANC_OVERRIDE; virtual void GetAllPublicIds(std::list& target, ResourceType resourceType, size_t since, - size_t limit); + size_t limit) + ORTHANC_OVERRIDE; virtual void GetChanges(std::list& target /*out*/, bool& done /*out*/, int64_t since, - uint32_t maxResults); + uint32_t maxResults) + ORTHANC_OVERRIDE; virtual void GetChildrenInternalId(std::list& target, - int64_t id); + int64_t id) + ORTHANC_OVERRIDE; virtual void GetChildrenPublicId(std::list& target, - int64_t id); + int64_t id) + ORTHANC_OVERRIDE; virtual void GetExportedResources(std::list& target /*out*/, bool& done /*out*/, int64_t since, - uint32_t maxResults); + uint32_t maxResults) + ORTHANC_OVERRIDE; - virtual void GetLastChange(std::list& target /*out*/); + virtual void GetLastChange(std::list& target /*out*/) + ORTHANC_OVERRIDE; - virtual void GetLastExportedResource(std::list& target /*out*/); + virtual void GetLastExportedResource(std::list& target /*out*/) + ORTHANC_OVERRIDE; virtual void GetMainDicomTags(DicomMap& map, - int64_t id); + int64_t id) + ORTHANC_OVERRIDE; - virtual std::string GetPublicId(int64_t resourceId); + virtual std::string GetPublicId(int64_t resourceId) + ORTHANC_OVERRIDE; - virtual uint64_t GetResourceCount(ResourceType resourceType); + virtual uint64_t GetResourceCount(ResourceType resourceType) + ORTHANC_OVERRIDE; - virtual ResourceType GetResourceType(int64_t resourceId); + virtual ResourceType GetResourceType(int64_t resourceId) + ORTHANC_OVERRIDE; - virtual uint64_t GetTotalCompressedSize(); + virtual uint64_t GetTotalCompressedSize() + ORTHANC_OVERRIDE; - virtual uint64_t GetTotalUncompressedSize(); + virtual uint64_t GetTotalUncompressedSize() + ORTHANC_OVERRIDE; - virtual bool IsExistingResource(int64_t internalId); + virtual bool IsExistingResource(int64_t internalId) + ORTHANC_OVERRIDE; - virtual bool IsProtectedPatient(int64_t internalId); + virtual bool IsProtectedPatient(int64_t internalId) + ORTHANC_OVERRIDE; virtual void ListAvailableMetadata(std::list& target, - int64_t id); + int64_t id) + ORTHANC_OVERRIDE; virtual void ListAvailableAttachments(std::list& target, - int64_t id); + int64_t id) + ORTHANC_OVERRIDE; virtual void LogChange(int64_t internalId, - const ServerIndexChange& change); + const ServerIndexChange& change) + ORTHANC_OVERRIDE; - virtual void LogExportedResource(const ExportedResource& resource); + virtual void LogExportedResource(const ExportedResource& resource) + ORTHANC_OVERRIDE; virtual bool LookupAttachment(FileInfo& attachment, int64_t id, - FileContentType contentType); + FileContentType contentType) + ORTHANC_OVERRIDE; virtual bool LookupGlobalProperty(std::string& target, - GlobalProperty property); + GlobalProperty property) + ORTHANC_OVERRIDE; + + virtual bool LookupMetadata(std::string& target, + int64_t id, + MetadataType type) + ORTHANC_OVERRIDE; + + virtual bool LookupParent(int64_t& parentId, + int64_t resourceId) + ORTHANC_OVERRIDE; + + virtual bool LookupResource(int64_t& id, + ResourceType& type, + const std::string& publicId) + ORTHANC_OVERRIDE; + + virtual bool SelectPatientToRecycle(int64_t& internalId) + ORTHANC_OVERRIDE; + + virtual bool SelectPatientToRecycle(int64_t& internalId, + int64_t patientIdToAvoid) + ORTHANC_OVERRIDE; + + virtual void SetGlobalProperty(GlobalProperty property, + const std::string& value) + ORTHANC_OVERRIDE; + + virtual void ClearMainDicomTags(int64_t id) + ORTHANC_OVERRIDE; + + virtual void SetMainDicomTag(int64_t id, + const DicomTag& tag, + const std::string& value) + ORTHANC_OVERRIDE; + + virtual void SetIdentifierTag(int64_t id, + const DicomTag& tag, + const std::string& value) + ORTHANC_OVERRIDE; + + virtual void SetMetadata(int64_t id, + MetadataType type, + const std::string& value) + ORTHANC_OVERRIDE; + virtual void SetProtectedPatient(int64_t internalId, + bool isProtected) + ORTHANC_OVERRIDE; + + virtual IDatabaseWrapper::ITransaction* StartTransaction() + ORTHANC_OVERRIDE; + + virtual void SetListener(IDatabaseListener& listener) + ORTHANC_OVERRIDE + { + listener_ = &listener; + } + + virtual unsigned int GetDatabaseVersion() + ORTHANC_OVERRIDE; + + virtual void Upgrade(unsigned int targetVersion, + IStorageArea& storageArea) + ORTHANC_OVERRIDE; + + void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer); + + virtual bool IsDiskSizeAbove(uint64_t threshold) + ORTHANC_OVERRIDE; + + virtual void ApplyLookupResources(std::list& resourcesId, + std::list* instancesId, + const std::vector& lookup, + ResourceType queryLevel, + size_t limit) + ORTHANC_OVERRIDE; + + virtual bool CreateInstance(CreateInstanceResult& result, + int64_t& instanceId, + const std::string& patient, + const std::string& study, + const std::string& series, + const std::string& instance) + ORTHANC_OVERRIDE; + + // From the "ILookupResources" interface + virtual void GetAllInternalIds(std::list& target, + ResourceType resourceType) + ORTHANC_OVERRIDE; + + // From the "ILookupResources" interface virtual void LookupIdentifier(std::list& result, ResourceType level, const DicomTag& tag, - IdentifierConstraintType type, - const std::string& value); - + Compatibility::IdentifierConstraintType type, + const std::string& value) + ORTHANC_OVERRIDE; + + // From the "ILookupResources" interface virtual void LookupIdentifierRange(std::list& result, ResourceType level, const DicomTag& tag, const std::string& start, - const std::string& end); - - virtual bool LookupMetadata(std::string& target, - int64_t id, - MetadataType type); - - virtual bool LookupParent(int64_t& parentId, - int64_t resourceId); + const std::string& end) + ORTHANC_OVERRIDE; - virtual bool LookupResource(int64_t& id, - ResourceType& type, - const std::string& publicId); - - virtual bool SelectPatientToRecycle(int64_t& internalId); - - virtual bool SelectPatientToRecycle(int64_t& internalId, - int64_t patientIdToAvoid); - - virtual void SetGlobalProperty(GlobalProperty property, - const std::string& value); - - virtual void ClearMainDicomTags(int64_t id); + virtual void SetResourcesContent(const Orthanc::ResourcesContent& content) + ORTHANC_OVERRIDE; - virtual void SetMainDicomTag(int64_t id, - const DicomTag& tag, - const std::string& value); - - virtual void SetIdentifierTag(int64_t id, - const DicomTag& tag, - const std::string& value); - - virtual void SetMetadata(int64_t id, - MetadataType type, - const std::string& value); + virtual void GetChildrenMetadata(std::list& target, + int64_t resourceId, + MetadataType metadata) + ORTHANC_OVERRIDE; - virtual void SetProtectedPatient(int64_t internalId, - bool isProtected); - - virtual SQLite::ITransaction* StartTransaction(); - - virtual void SetListener(IDatabaseListener& listener) - { - listener_ = &listener; - } - - virtual unsigned int GetDatabaseVersion(); - - virtual void Upgrade(unsigned int targetVersion, - IStorageArea& storageArea); - - void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer); + virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE; + + virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE; }; } diff -r 4cfed5c2eacd -r fc9a4a2dad63 Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Plugins/Engine/OrthancPlugins.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -38,6 +38,10 @@ #error The plugin support is disabled #endif +#if !defined(DCMTK_VERSION_NUMBER) +# error The macro DCMTK_VERSION_NUMBER must be defined +#endif + #include "../../Core/ChunkedBuffer.h" #include "../../Core/DicomFormat/DicomArray.h" @@ -1644,7 +1648,7 @@ throw OrthancException(ErrorCode_InternalError); } - std::list result; + std::vector result; { PImpl::ServerContextLock lock(*pimpl_); @@ -1653,7 +1657,7 @@ if (result.size() == 1) { - *p.result = CopyString(result.front()); + *p.result = CopyString(result[0]); } else { @@ -2416,7 +2420,11 @@ ~DictionaryReadLocker() { +#if DCMTK_VERSION_NUMBER >= 364 + dcmDataDict.rdunlock(); +#else dcmDataDict.unlock(); +#endif } const DcmDataDictionary* operator->() diff -r 4cfed5c2eacd -r fc9a4a2dad63 Plugins/Engine/PluginsEnumerations.cpp --- a/Plugins/Engine/PluginsEnumerations.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/Plugins/Engine/PluginsEnumerations.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -43,23 +43,23 @@ namespace Orthanc { - namespace Plugins + namespace Compatibility { - OrthancPluginResourceType Convert(ResourceType type) + OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint) { - switch (type) + switch (constraint) { - case ResourceType_Patient: - return OrthancPluginResourceType_Patient; + case Compatibility::IdentifierConstraintType_Equal: + return OrthancPluginIdentifierConstraint_Equal; - case ResourceType_Study: - return OrthancPluginResourceType_Study; + case Compatibility::IdentifierConstraintType_GreaterOrEqual: + return OrthancPluginIdentifierConstraint_GreaterOrEqual; - case ResourceType_Series: - return OrthancPluginResourceType_Series; + case Compatibility::IdentifierConstraintType_SmallerOrEqual: + return OrthancPluginIdentifierConstraint_SmallerOrEqual; - case ResourceType_Instance: - return OrthancPluginResourceType_Instance; + case Compatibility::IdentifierConstraintType_Wildcard: + return OrthancPluginIdentifierConstraint_Wildcard; default: throw OrthancException(ErrorCode_ParameterOutOfRange); @@ -67,28 +67,31 @@ } - ResourceType Convert(OrthancPluginResourceType type) + IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint) { - switch (type) + switch (constraint) { - case OrthancPluginResourceType_Patient: - return ResourceType_Patient; + case OrthancPluginIdentifierConstraint_Equal: + return Compatibility::IdentifierConstraintType_Equal; - case OrthancPluginResourceType_Study: - return ResourceType_Study; + case OrthancPluginIdentifierConstraint_GreaterOrEqual: + return Compatibility::IdentifierConstraintType_GreaterOrEqual; - case OrthancPluginResourceType_Series: - return ResourceType_Series; + case OrthancPluginIdentifierConstraint_SmallerOrEqual: + return Compatibility::IdentifierConstraintType_SmallerOrEqual; - case OrthancPluginResourceType_Instance: - return ResourceType_Instance; + case OrthancPluginIdentifierConstraint_Wildcard: + return Compatibility::IdentifierConstraintType_Wildcard; default: throw OrthancException(ErrorCode_ParameterOutOfRange); } } + } + namespace Plugins + { OrthancPluginChangeType Convert(ChangeType type) { switch (type) @@ -266,50 +269,6 @@ } - OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint) - { - switch (constraint) - { - case IdentifierConstraintType_Equal: - return OrthancPluginIdentifierConstraint_Equal; - - case IdentifierConstraintType_GreaterOrEqual: - return OrthancPluginIdentifierConstraint_GreaterOrEqual; - - case IdentifierConstraintType_SmallerOrEqual: - return OrthancPluginIdentifierConstraint_SmallerOrEqual; - - case IdentifierConstraintType_Wildcard: - return OrthancPluginIdentifierConstraint_Wildcard; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - - - IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint) - { - switch (constraint) - { - case OrthancPluginIdentifierConstraint_Equal: - return IdentifierConstraintType_Equal; - - case OrthancPluginIdentifierConstraint_GreaterOrEqual: - return IdentifierConstraintType_GreaterOrEqual; - - case OrthancPluginIdentifierConstraint_SmallerOrEqual: - return IdentifierConstraintType_SmallerOrEqual; - - case OrthancPluginIdentifierConstraint_Wildcard: - return IdentifierConstraintType_Wildcard; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - - OrthancPluginInstanceOrigin Convert(RequestOrigin origin) { switch (origin) diff -r 4cfed5c2eacd -r fc9a4a2dad63 Plugins/Engine/PluginsEnumerations.h --- a/Plugins/Engine/PluginsEnumerations.h Thu Jan 24 10:54:47 2019 +0100 +++ b/Plugins/Engine/PluginsEnumerations.h Thu Jan 24 10:55:19 2019 +0100 @@ -35,17 +35,27 @@ #if ORTHANC_ENABLE_PLUGINS == 1 +/** + * NB: Conversions to/from "OrthancPluginConstraintType" and + * "OrthancPluginResourceType" are located in file + * "../../OrthancServer/Search/DatabaseConstraint.h" to be shared with + * the "orthanc-databases" project. + **/ + #include "../Include/orthanc/OrthancCPlugin.h" -#include "../../OrthancServer/ServerEnumerations.h" +#include "../../OrthancServer/Search/DatabaseConstraint.h" namespace Orthanc { + namespace Compatibility + { + OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint); + + IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint); + } + namespace Plugins { - OrthancPluginResourceType Convert(ResourceType type); - - ResourceType Convert(OrthancPluginResourceType type); - OrthancPluginChangeType Convert(ChangeType type); OrthancPluginPixelFormat Convert(PixelFormat format); @@ -58,10 +68,6 @@ DicomToJsonFormat Convert(OrthancPluginDicomToJsonFormat format); - OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint); - - IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint); - OrthancPluginInstanceOrigin Convert(RequestOrigin origin); OrthancPluginHttpMethod Convert(HttpMethod method); diff -r 4cfed5c2eacd -r fc9a4a2dad63 Plugins/Include/orthanc/OrthancCDatabasePlugin.h --- a/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Thu Jan 24 10:54:47 2019 +0100 +++ b/Plugins/Include/orthanc/OrthancCDatabasePlugin.h Thu Jan 24 10:55:19 2019 +0100 @@ -75,6 +75,7 @@ _OrthancPluginDatabaseAnswerType_Int64 = 15, _OrthancPluginDatabaseAnswerType_Resource = 16, _OrthancPluginDatabaseAnswerType_String = 17, + _OrthancPluginDatabaseAnswerType_MatchingResource = 18, /* New in Orthanc 1.5.2 */ _OrthancPluginDatabaseAnswerType_INTERNAL = 0x7fffffff } _OrthancPluginDatabaseAnswerType; @@ -120,6 +121,55 @@ const char* sopInstanceUid; } OrthancPluginExportedResource; + typedef struct /* New in Orthanc 1.5.2 */ + { + OrthancPluginResourceType level; + uint16_t tagGroup; + uint16_t tagElement; + uint8_t isIdentifierTag; + uint8_t isCaseSensitive; + uint8_t isMandatory; + OrthancPluginConstraintType type; + uint32_t valuesCount; + const char* const* values; + } OrthancPluginDatabaseConstraint; + + typedef struct /* New in Orthanc 1.5.2 */ + { + const char* resourceId; + const char* someInstanceId; /* Can be NULL if not requested */ + } OrthancPluginMatchingResource; + + typedef struct /* New in Orthanc 1.5.2 */ + { + /* Mandatory field */ + uint8_t isNewInstance; + int64_t instanceId; + + /* The following fields must only be set if "isNewInstance" is "true" */ + uint8_t isNewPatient; + uint8_t isNewStudy; + uint8_t isNewSeries; + int64_t patientId; + int64_t studyId; + int64_t seriesId; + } OrthancPluginCreateInstanceResult; + + typedef struct /* New in Orthanc 1.5.2 */ + { + int64_t resource; + uint16_t group; + uint16_t element; + const char* value; + } OrthancPluginResourcesContentTags; + + typedef struct /* New in Orthanc 1.5.2 */ + { + int64_t resource; + int32_t metadata; + const char* value; + } OrthancPluginResourcesContentMetadata; + typedef struct { @@ -272,6 +322,19 @@ context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); } + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerMatchingResource( + OrthancPluginContext* context, + OrthancPluginDatabaseContext* database, + const OrthancPluginMatchingResource* match) + { + _OrthancPluginDatabaseAnswer params; + memset(¶ms, 0, sizeof(params)); + params.database = database; + params.type = _OrthancPluginDatabaseAnswerType_MatchingResource; + params.valueGeneric = match; + context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, ¶ms); + } + ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedAttachment( OrthancPluginContext* context, OrthancPluginDatabaseContext* database, @@ -638,6 +701,10 @@ typedef struct { + /** + * Base extensions since Orthanc 1.0.0 + **/ + /* Output: Use OrthancPluginDatabaseAnswerString() */ OrthancPluginErrorCode (*getAllPublicIdsWithLimit) ( /* outputs */ @@ -683,6 +750,11 @@ const OrthancPluginDicomTag* tag, OrthancPluginIdentifierConstraint constraint); + + /** + * Extensions since Orthanc 1.4.0 + **/ + /* Output: Use OrthancPluginDatabaseAnswerInt64() */ OrthancPluginErrorCode (*lookupIdentifierRange) ( /* outputs */ @@ -694,7 +766,66 @@ uint16_t element, const char* start, const char* end); - } OrthancPluginDatabaseExtensions; + + + /** + * Extensions since Orthanc 1.5.2 + **/ + + /* Ouput: Use OrthancPluginDatabaseAnswerMatchingResource */ + OrthancPluginErrorCode (*lookupResources) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + uint32_t constraintsCount, + const OrthancPluginDatabaseConstraint* constraints, + OrthancPluginResourceType queryLevel, + uint32_t limit, + uint8_t requestSomeInstance); + + + OrthancPluginErrorCode (*createInstance) ( + /* output */ + OrthancPluginCreateInstanceResult* output, + /* inputs */ + void* payload, + const char* hashPatient, + const char* hashStudy, + const char* hashSeries, + const char* hashInstance); + + OrthancPluginErrorCode (*setResourcesContent) ( + /* inputs */ + void* payload, + uint32_t countIdentifierTags, + const OrthancPluginResourcesContentTags* identifierTags, + uint32_t countMainDicomTags, + const OrthancPluginResourcesContentTags* mainDicomTags, + uint32_t countMetadata, + const OrthancPluginResourcesContentMetadata* metadata); + + /* Ouput: Use OrthancPluginDatabaseAnswerString */ + OrthancPluginErrorCode (*getChildrenMetadata) ( + /* outputs */ + OrthancPluginDatabaseContext* context, + /* inputs */ + void* payload, + int64_t resourceId, + int32_t metadata); + + OrthancPluginErrorCode (*getLastChangeIndex) ( + /* outputs */ + int64_t* target, + /* inputs */ + void* payload); + + OrthancPluginErrorCode (*tagMostRecentPatient) ( + /* inputs */ + void* payload, + int64_t patientId); + + } OrthancPluginDatabaseExtensions; /*. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "LookupIdentifierQuery.h" + +#include "../../Core/DicomParsing/FromDcmtkBridge.h" +#include "../../Core/OrthancException.h" +#include "../ServerToolbox.h" +#include "SetOfResources.h" + +#include + + + +namespace Orthanc +{ + LookupIdentifierQuery::SingleConstraint:: + SingleConstraint(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value) : + tag_(tag), + type_(type), + value_(ServerToolbox::NormalizeIdentifier(value)) + { + } + + + LookupIdentifierQuery::RangeConstraint:: + RangeConstraint(const DicomTag& tag, + const std::string& start, + const std::string& end) : + tag_(tag), + start_(ServerToolbox::NormalizeIdentifier(start)), + end_(ServerToolbox::NormalizeIdentifier(end)) + { + } + + + LookupIdentifierQuery::Disjunction::~Disjunction() + { + for (size_t i = 0; i < singleConstraints_.size(); i++) + { + delete singleConstraints_[i]; + } + + for (size_t i = 0; i < rangeConstraints_.size(); i++) + { + delete rangeConstraints_[i]; + } + } + + + void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value) + { + singleConstraints_.push_back(new SingleConstraint(tag, type, value)); + } + + + void LookupIdentifierQuery::Disjunction::AddRange(const DicomTag& tag, + const std::string& start, + const std::string& end) + { + rangeConstraints_.push_back(new RangeConstraint(tag, start, end)); + } + + + LookupIdentifierQuery::~LookupIdentifierQuery() + { + for (Disjunctions::iterator it = disjunctions_.begin(); + it != disjunctions_.end(); ++it) + { + delete *it; + } + } + + + bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag) + { + return ServerToolbox::IsIdentifier(tag, level_); + } + + + void LookupIdentifierQuery::AddConstraint(DicomTag tag, + IdentifierConstraintType type, + const std::string& value) + { + assert(IsIdentifier(tag)); + disjunctions_.push_back(new Disjunction); + disjunctions_.back()->Add(tag, type, value); + } + + + void LookupIdentifierQuery::AddRange(DicomTag tag, + const std::string& start, + const std::string& end) + { + assert(IsIdentifier(tag)); + disjunctions_.push_back(new Disjunction); + disjunctions_.back()->AddRange(tag, start, end); + } + + + LookupIdentifierQuery::Disjunction& LookupIdentifierQuery::AddDisjunction() + { + disjunctions_.push_back(new Disjunction); + return *disjunctions_.back(); + } + + + void LookupIdentifierQuery::Apply(std::list& result, + IDatabaseWrapper& database) + { + SetOfResources resources(database, level_); + Apply(resources, database); + + resources.Flatten(result); + } + + + void LookupIdentifierQuery::Apply(SetOfResources& result, + IDatabaseWrapper& database) + { + for (size_t i = 0; i < disjunctions_.size(); i++) + { + std::list a; + + for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++) + { + const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j); + std::list b; + database.LookupIdentifier(b, level_, constraint.GetTag(), + constraint.GetType(), constraint.GetValue()); + + a.splice(a.end(), b); + } + + for (size_t j = 0; j < disjunctions_[i]->GetRangeConstraintsCount(); j++) + { + const RangeConstraint& constraint = disjunctions_[i]->GetRangeConstraint(j); + std::list b; + database.LookupIdentifierRange(b, level_, constraint.GetTag(), + constraint.GetStart(), constraint.GetEnd()); + + a.splice(a.end(), b); + } + + result.Intersect(a); + } + } + + + void LookupIdentifierQuery::Print(std::ostream& s) const + { + s << "Constraint: " << std::endl; + for (Disjunctions::const_iterator + it = disjunctions_.begin(); it != disjunctions_.end(); ++it) + { + if (it == disjunctions_.begin()) + s << " "; + else + s << "OR "; + + for (size_t j = 0; j < (*it)->GetSingleConstraintsCount(); j++) + { + const SingleConstraint& c = (*it)->GetSingleConstraint(j); + s << FromDcmtkBridge::GetTagName(c.GetTag(), ""); + + switch (c.GetType()) + { + case IdentifierConstraintType_Equal: s << " == "; break; + case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break; + case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break; + case IdentifierConstraintType_Wildcard: s << " ~= "; break; + default: + s << " ? "; + } + + s << c.GetValue() << std::endl; + } + } + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,207 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "../IDatabaseWrapper.h" + +#include "SetOfResources.h" + +#include +#include + +namespace Orthanc +{ + /** + * Primitive for wildcard matching, as defined in DICOM: + * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4 + * + * "Any occurrence of an "*" or a "?", then "*" shall match any + * sequence of characters (including a zero length value) and "?" + * shall match any single character. This matching is case + * sensitive, except for Attributes with an PN Value + * Representation (e.g., Patient Name (0010,0010))." + * + * Pay attention to the fact that "*" (resp. "?") generally + * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The + * values "%", "_", "\" should in the user request should + * respectively be escaped as "\%", "\_" and "\\". + * + * This matching must be case sensitive: The special case of PN VR + * is taken into consideration by normalizing the query string in + * method "NormalizeIdentifier()". + **/ + + class LookupIdentifierQuery : public boost::noncopyable + { + // This class encodes a conjunction ("AND") of disjunctions. Each + // disjunction represents an "OR" of several constraints. + + public: + class SingleConstraint + { + private: + DicomTag tag_; + IdentifierConstraintType type_; + std::string value_; + + public: + SingleConstraint(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value); + + const DicomTag& GetTag() const + { + return tag_; + } + + IdentifierConstraintType GetType() const + { + return type_; + } + + const std::string& GetValue() const + { + return value_; + } + }; + + + class RangeConstraint + { + private: + DicomTag tag_; + std::string start_; + std::string end_; + + public: + RangeConstraint(const DicomTag& tag, + const std::string& start, + const std::string& end); + + const DicomTag& GetTag() const + { + return tag_; + } + + const std::string& GetStart() const + { + return start_; + } + + const std::string& GetEnd() const + { + return end_; + } + }; + + + class Disjunction : public boost::noncopyable + { + private: + std::vector singleConstraints_; + std::vector rangeConstraints_; + + public: + ~Disjunction(); + + void Add(const DicomTag& tag, + IdentifierConstraintType type, + const std::string& value); + + void AddRange(const DicomTag& tag, + const std::string& start, + const std::string& end); + + size_t GetSingleConstraintsCount() const + { + return singleConstraints_.size(); + } + + const SingleConstraint& GetSingleConstraint(size_t i) const + { + return *singleConstraints_[i]; + } + + size_t GetRangeConstraintsCount() const + { + return rangeConstraints_.size(); + } + + const RangeConstraint& GetRangeConstraint(size_t i) const + { + return *rangeConstraints_[i]; + } + }; + + + private: + typedef std::vector Disjunctions; + + ResourceType level_; + Disjunctions disjunctions_; + + public: + LookupIdentifierQuery(ResourceType level) : level_(level) + { + } + + ~LookupIdentifierQuery(); + + bool IsIdentifier(const DicomTag& tag); + + void AddConstraint(DicomTag tag, + IdentifierConstraintType type, + const std::string& value); + + void AddRange(DicomTag tag, + const std::string& start, + const std::string& end); + + Disjunction& AddDisjunction(); + + ResourceType GetLevel() const + { + return level_; + } + + // The database must be locked + void Apply(std::list& result, + IDatabaseWrapper& database); + + void Apply(SetOfResources& result, + IDatabaseWrapper& database); + + void Print(std::ostream& s) const; + }; +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,479 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "LookupResource.h" + +#include "../../Core/OrthancException.h" +#include "../../Core/FileStorage/StorageAccessor.h" +#include "../ServerToolbox.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" + + +namespace Orthanc +{ + static bool DoesDicomMapMatch(const DicomMap& dicom, + const DicomTag& tag, + const IFindConstraint& constraint) + { + const DicomValue* value = dicom.TestAndGetValue(tag); + + return (value != NULL && + !value->IsNull() && + !value->IsBinary() && + constraint.Match(value->GetContent())); + } + + + LookupResource::Level::Level(ResourceType level) : level_(level) + { + const DicomTag* tags = NULL; + size_t size; + + ServerToolbox::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + identifiers_.insert(tags[i]); + } + + DicomMap::LoadMainDicomTags(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (identifiers_.find(tags[i]) == identifiers_.end()) + { + mainTags_.insert(tags[i]); + } + } + } + + LookupResource::Level::~Level() + { + for (Constraints::iterator it = mainTagsConstraints_.begin(); + it != mainTagsConstraints_.end(); ++it) + { + delete it->second; + } + + for (Constraints::iterator it = identifiersConstraints_.begin(); + it != identifiersConstraints_.end(); ++it) + { + delete it->second; + } + } + + bool LookupResource::Level::Add(const DicomTag& tag, + std::auto_ptr& constraint) + { + if (identifiers_.find(tag) != identifiers_.end()) + { + if (level_ == ResourceType_Patient) + { + // The filters on the patient level must be cloned to the study level + identifiersConstraints_[tag] = constraint->Clone(); + } + else + { + identifiersConstraints_[tag] = constraint.release(); + } + + return true; + } + else if (mainTags_.find(tag) != mainTags_.end()) + { + if (level_ == ResourceType_Patient) + { + // The filters on the patient level must be cloned to the study level + mainTagsConstraints_[tag] = constraint->Clone(); + } + else + { + mainTagsConstraints_[tag] = constraint.release(); + } + + return true; + } + else + { + // This is not a main DICOM tag + return false; + } + } + + + bool LookupResource::Level::IsMatch(const DicomMap& dicom) const + { + for (Constraints::const_iterator it = identifiersConstraints_.begin(); + it != identifiersConstraints_.end(); ++it) + { + assert(it->second != NULL); + + if (!DoesDicomMapMatch(dicom, it->first, *it->second)) + { + return false; + } + } + + for (Constraints::const_iterator it = mainTagsConstraints_.begin(); + it != mainTagsConstraints_.end(); ++it) + { + assert(it->second != NULL); + + if (!DoesDicomMapMatch(dicom, it->first, *it->second)) + { + return false; + } + } + + return true; + } + + + LookupResource::LookupResource(ResourceType level) : level_(level) + { + switch (level) + { + case ResourceType_Patient: + levels_[ResourceType_Patient] = new Level(ResourceType_Patient); + break; + + case ResourceType_Instance: + levels_[ResourceType_Instance] = new Level(ResourceType_Instance); + // Do not add "break" here + + case ResourceType_Series: + levels_[ResourceType_Series] = new Level(ResourceType_Series); + // Do not add "break" here + + case ResourceType_Study: + levels_[ResourceType_Study] = new Level(ResourceType_Study); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + LookupResource::~LookupResource() + { + for (Levels::iterator it = levels_.begin(); + it != levels_.end(); ++it) + { + delete it->second; + } + + for (Constraints::iterator it = unoptimizedConstraints_.begin(); + it != unoptimizedConstraints_.end(); ++it) + { + delete it->second; + } + } + + + + bool LookupResource::AddInternal(ResourceType level, + const DicomTag& tag, + std::auto_ptr& constraint) + { + Levels::iterator it = levels_.find(level); + if (it != levels_.end()) + { + if (it->second->Add(tag, constraint)) + { + return true; + } + } + + return false; + } + + + void LookupResource::Add(const DicomTag& tag, + IFindConstraint* constraint) + { + std::auto_ptr c(constraint); + + if (!AddInternal(ResourceType_Patient, tag, c) && + !AddInternal(ResourceType_Study, tag, c) && + !AddInternal(ResourceType_Series, tag, c) && + !AddInternal(ResourceType_Instance, tag, c)) + { + unoptimizedConstraints_[tag] = c.release(); + } + } + + + static bool Match(const DicomMap& tags, + const DicomTag& tag, + const IFindConstraint& constraint) + { + const DicomValue* value = tags.TestAndGetValue(tag); + + if (value == NULL || + value->IsNull() || + value->IsBinary()) + { + return false; + } + else + { + return constraint.Match(value->GetContent()); + } + } + + + void LookupResource::Level::Apply(SetOfResources& candidates, + IDatabaseWrapper& database) const + { + // First, use the indexed identifiers + LookupIdentifierQuery query(level_); + + for (Constraints::const_iterator it = identifiersConstraints_.begin(); + it != identifiersConstraints_.end(); ++it) + { + it->second->Setup(query, it->first); + } + + query.Apply(candidates, database); + + /*{ + query.Print(std::cout); + std::list source; + candidates.Flatten(source); + printf("=> %d\n", source.size()); + }*/ + + // Secondly, filter using the main DICOM tags + if (!identifiersConstraints_.empty() || + !mainTagsConstraints_.empty()) + { + std::list source; + candidates.Flatten(source); + candidates.Clear(); + + std::list filtered; + for (std::list::const_iterator candidate = source.begin(); + candidate != source.end(); ++candidate) + { + DicomMap tags; + database.GetMainDicomTags(tags, *candidate); + + bool match = true; + + // Re-apply the identifier constraints, as their "Setup" + // method is less restrictive than their "Match" method + for (Constraints::const_iterator it = identifiersConstraints_.begin(); + match && it != identifiersConstraints_.end(); ++it) + { + if (!Match(tags, it->first, *it->second)) + { + match = false; + } + } + + for (Constraints::const_iterator it = mainTagsConstraints_.begin(); + match && it != mainTagsConstraints_.end(); ++it) + { + if (!Match(tags, it->first, *it->second)) + { + match = false; + } + } + + if (match) + { + filtered.push_back(*candidate); + } + } + + candidates.Intersect(filtered); + } + } + + + + bool LookupResource::IsMatch(const DicomMap& dicom) const + { + for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it) + { + if (!it->second->IsMatch(dicom)) + { + return false; + } + } + + for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); + it != unoptimizedConstraints_.end(); ++it) + { + assert(it->second != NULL); + + if (!DoesDicomMapMatch(dicom, it->first, *it->second)) + { + return false; + } + } + + return true; + } + + + void LookupResource::ApplyLevel(SetOfResources& candidates, + ResourceType level, + IDatabaseWrapper& database) const + { + Levels::const_iterator it = levels_.find(level); + if (it != levels_.end()) + { + it->second->Apply(candidates, database); + } + + if (level == ResourceType_Study && + modalitiesInStudy_.get() != NULL) + { + // There is a constraint on the "ModalitiesInStudy" DICOM + // extension. Check out whether one child series has one of the + // allowed modalities + std::list allStudies, matchingStudies; + candidates.Flatten(allStudies); + + for (std::list::const_iterator + study = allStudies.begin(); study != allStudies.end(); ++study) + { + std::list childrenSeries; + database.GetChildrenInternalId(childrenSeries, *study); + + for (std::list::const_iterator + series = childrenSeries.begin(); series != childrenSeries.end(); ++series) + { + DicomMap tags; + database.GetMainDicomTags(tags, *series); + + const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY); + if (value != NULL && + !value->IsNull() && + !value->IsBinary()) + { + if (modalitiesInStudy_->Match(value->GetContent())) + { + matchingStudies.push_back(*study); + break; + } + } + } + } + + candidates.Intersect(matchingStudies); + } + } + + + void LookupResource::FindCandidates(std::list& result, + IDatabaseWrapper& database) const + { + ResourceType startingLevel; + if (level_ == ResourceType_Patient) + { + startingLevel = ResourceType_Patient; + } + else + { + startingLevel = ResourceType_Study; + } + + SetOfResources candidates(database, startingLevel); + + switch (level_) + { + case ResourceType_Patient: + ApplyLevel(candidates, ResourceType_Patient, database); + break; + + case ResourceType_Study: + ApplyLevel(candidates, ResourceType_Study, database); + break; + + case ResourceType_Series: + ApplyLevel(candidates, ResourceType_Study, database); + candidates.GoDown(); + ApplyLevel(candidates, ResourceType_Series, database); + break; + + case ResourceType_Instance: + ApplyLevel(candidates, ResourceType_Study, database); + candidates.GoDown(); + ApplyLevel(candidates, ResourceType_Series, database); + candidates.GoDown(); + ApplyLevel(candidates, ResourceType_Instance, database); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + candidates.Flatten(result); + } + + + void LookupResource::SetModalitiesInStudy(const std::string& modalities) + { + modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */)); + + std::vector items; + Toolbox::TokenizeString(items, modalities, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + modalitiesInStudy_->AddAllowedValue(items[i]); + } + } + + + void LookupResource::AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive) + { + // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained + // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html + if (tag == DICOM_TAG_MODALITIES_IN_STUDY) + { + SetModalitiesInStudy(dicomQuery); + } + else + { + Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive)); + } + } + +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/Graveyard/DatabaseOptimizations/LookupResource.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/DatabaseOptimizations/LookupResource.h Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,115 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 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 "ListConstraint.h" +#include "SetOfResources.h" + +#include + +namespace Orthanc +{ + class LookupResource : public boost::noncopyable + { + private: + typedef std::map Constraints; + + class Level + { + private: + ResourceType level_; + std::set identifiers_; + std::set mainTags_; + Constraints identifiersConstraints_; + Constraints mainTagsConstraints_; + + public: + Level(ResourceType level); + + ~Level(); + + bool Add(const DicomTag& tag, + std::auto_ptr& constraint); + + void Apply(SetOfResources& candidates, + IDatabaseWrapper& database) const; + + bool IsMatch(const DicomMap& dicom) const; + }; + + typedef std::map Levels; + + ResourceType level_; + Levels levels_; + Constraints unoptimizedConstraints_; // Constraints on non-main DICOM tags + std::auto_ptr modalitiesInStudy_; + + bool AddInternal(ResourceType level, + const DicomTag& tag, + std::auto_ptr& constraint); + + void ApplyLevel(SetOfResources& candidates, + ResourceType level, + IDatabaseWrapper& database) const; + + public: + LookupResource(ResourceType level); + + ~LookupResource(); + + ResourceType GetLevel() const + { + return level_; + } + + void SetModalitiesInStudy(const std::string& modalities); + + void Add(const DicomTag& tag, + IFindConstraint* constraint); // Takes ownership + + void AddDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive); + + void FindCandidates(std::list& result, + IDatabaseWrapper& database) const; + + bool HasOnlyMainDicomTags() const + { + return unoptimizedConstraints_.empty(); + } + + bool IsMatch(const DicomMap& dicom) const; + }; +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/Patches/civetweb-1.11.patch --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Patches/civetweb-1.11.patch Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,98 @@ +diff -urEb civetweb-1.11.orig/include/civetweb.h civetweb-1.11/include/civetweb.h +--- civetweb-1.11.orig/include/civetweb.h 2019-01-17 21:09:41.844888908 +0100 ++++ civetweb-1.11/include/civetweb.h 2019-01-21 12:05:08.138998659 +0100 +@@ -1507,6 +1507,10 @@ + #endif + + ++// Added by SJ ++CIVETWEB_API void mg_disable_keep_alive(struct mg_connection *conn); ++ ++ + #ifdef __cplusplus + } + #endif /* __cplusplus */ +diff -urEb civetweb-1.11.orig/src/civetweb.c civetweb-1.11/src/civetweb.c +--- civetweb-1.11.orig/src/civetweb.c 2019-01-17 21:09:41.852888857 +0100 ++++ civetweb-1.11/src/civetweb.c 2019-01-21 12:06:35.826868284 +0100 +@@ -59,6 +59,9 @@ + #if defined(__linux__) && !defined(_XOPEN_SOURCE) + #define _XOPEN_SOURCE 600 /* For flockfile() on Linux */ + #endif ++#if defined(__LSB_VERSION__) ++#define NEED_TIMEGM ++#endif + #if !defined(_LARGEFILE_SOURCE) + #define _LARGEFILE_SOURCE /* For fseeko(), ftello() */ + #endif +@@ -129,6 +132,12 @@ + + + /* Alternative queue is well tested and should be the new default */ ++#if defined(__LSB_VERSION__) ++/* Function "eventfd()" is not available in Linux Standard Base, can't ++ * use the alternative queue */ ++#define NO_ALTERNATIVE_QUEUE ++#endif ++ + #if defined(NO_ALTERNATIVE_QUEUE) + #if defined(ALTERNATIVE_QUEUE) + #error "Define ALTERNATIVE_QUEUE or NO_ALTERNATIVE_QUEUE or none, but not both" +@@ -536,6 +545,10 @@ + #if !defined(EWOULDBLOCK) + #define EWOULDBLOCK WSAEWOULDBLOCK + #endif /* !EWOULDBLOCK */ ++#if !defined(ECONNRESET) ++/* This macro is not defined e.g. in Visual Studio 2008 */ ++#define ECONNRESET WSAECONNRESET ++#endif /* !ECONNRESET */ + #define _POSIX_ + #define INT64_FMT "I64d" + #define UINT64_FMT "I64u" +@@ -2939,6 +2952,13 @@ + #endif + + ++#if defined(__LSB_VERSION__) ++static void ++mg_set_thread_name(const char *threadName) ++{ ++ /* prctl() does not seem to be available in Linux Standard Base */ ++} ++#else + static void + mg_set_thread_name(const char *name) + { +@@ -2980,6 +3000,7 @@ + (void)prctl(PR_SET_NAME, threadName, 0, 0, 0); + #endif + } ++#endif + #else /* !defined(NO_THREAD_NAME) */ + void + mg_set_thread_name(const char *threadName) +@@ -16919,6 +16940,10 @@ + /* Message is a valid request */ + + /* Is there a "host" ? */ ++ /* https://github.com/civetweb/civetweb/pull/675/commits/96e3e8c50acb4b8e0c946d02b5f880a3e62986e1 */ ++ if (conn->host!=NULL) { ++ mg_free((void *)conn->host); ++ } + conn->host = alloc_get_host(conn); + if (!conn->host) { + mg_snprintf(conn, +@@ -19857,4 +19882,13 @@ + } + + ++// Added by SJ ++void mg_disable_keep_alive(struct mg_connection *conn) ++{ ++ if (conn != NULL) { ++ conn->must_close = 1; ++ } ++} ++ ++ + /* End of civetweb.c */ diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/Patches/mongoose-3.8-patch.diff --- a/Resources/Patches/mongoose-3.8-patch.diff Thu Jan 24 10:54:47 2019 +0100 +++ b/Resources/Patches/mongoose-3.8-patch.diff Thu Jan 24 10:55:19 2019 +0100 @@ -1,5 +1,5 @@ ---- mongoose.c.orig 2014-09-01 11:25:18.223466994 +0200 -+++ mongoose.c 2014-09-01 11:30:21.807479338 +0200 +--- mongoose.c.orig 2019-01-14 13:06:27.147098524 +0100 ++++ mongoose.c 2019-01-14 12:44:35.331361929 +0100 @@ -50,6 +50,14 @@ #define PATH_MAX FILENAME_MAX #endif // __SYMBIAN32__ @@ -27,3 +27,80 @@ #endif // _MSC_VER #define ERRNO GetLastError() +@@ -2997,19 +3006,19 @@ + } + } + +-static int is_valid_http_method(const char *method) { +- return !strcmp(method, "GET") || !strcmp(method, "POST") || ++static int is_valid_http_method(const char *method, int *isValidHttpMethod) { ++ *isValidHttpMethod = !strcmp(method, "GET") || !strcmp(method, "POST") || + !strcmp(method, "HEAD") || !strcmp(method, "CONNECT") || + !strcmp(method, "PUT") || !strcmp(method, "DELETE") || + !strcmp(method, "OPTIONS") || !strcmp(method, "PROPFIND") +- || !strcmp(method, "MKCOL") +- ; ++ || !strcmp(method, "MKCOL"); ++ return *isValidHttpMethod; + } + + // Parse HTTP request, fill in mg_request_info structure. + // This function modifies the buffer by NUL-terminating + // HTTP request components, header names and header values. +-static int parse_http_message(char *buf, int len, struct mg_request_info *ri) { ++static int parse_http_message(char *buf, int len, struct mg_request_info *ri, int *isValidHttpMethod) { + int is_request, request_length = get_request_len(buf, len); + if (request_length > 0) { + // Reset attributes. DO NOT TOUCH is_ssl, remote_ip, remote_port +@@ -3025,7 +3034,7 @@ + ri->request_method = skip(&buf, " "); + ri->uri = skip(&buf, " "); + ri->http_version = skip(&buf, "\r\n"); +- if (((is_request = is_valid_http_method(ri->request_method)) && ++ if (((is_request = is_valid_http_method(ri->request_method, isValidHttpMethod)) && + memcmp(ri->http_version, "HTTP/", 5) != 0) || + (!is_request && memcmp(ri->request_method, "HTTP/", 5)) != 0) { + request_length = -1; +@@ -4930,7 +4939,7 @@ + return uri[0] == '/' || (uri[0] == '*' && uri[1] == '\0'); + } + +-static int getreq(struct mg_connection *conn, char *ebuf, size_t ebuf_len) { ++static int getreq(struct mg_connection *conn, char *ebuf, size_t ebuf_len, int *isValidHttpMethod) { + const char *cl; + + ebuf[0] = '\0'; +@@ -4944,7 +4953,7 @@ + } else if (conn->request_len <= 0) { + snprintf(ebuf, ebuf_len, "%s", "Client closed connection"); + } else if (parse_http_message(conn->buf, conn->buf_size, +- &conn->request_info) <= 0) { ++ &conn->request_info, isValidHttpMethod) <= 0) { + snprintf(ebuf, ebuf_len, "Bad request: [%.*s]", conn->data_len, conn->buf); + } else { + // Request is valid +@@ -4973,7 +4982,8 @@ + } else if (mg_vprintf(conn, fmt, ap) <= 0) { + snprintf(ebuf, ebuf_len, "%s", "Error sending request"); + } else { +- getreq(conn, ebuf, ebuf_len); ++ int isValidHttpMethod = 1; /* unused in this case */ ++ getreq(conn, ebuf, ebuf_len, &isValidHttpMethod); + } + if (ebuf[0] != '\0' && conn != NULL) { + mg_close_connection(conn); +@@ -4995,8 +5005,13 @@ + // to crule42. + conn->data_len = 0; + do { +- if (!getreq(conn, ebuf, sizeof(ebuf))) { ++ int isValidHttpMethod = 1; ++ if (!getreq(conn, ebuf, sizeof(ebuf), &isValidHttpMethod)) { ++ if (isValidHttpMethod) { + send_http_error(conn, 500, "Server Error", "%s", ebuf); ++ } else { ++ send_http_error(conn, 400, "Bad Request", "%s", ebuf); ++ } + conn->must_close = 1; + } else if (!is_valid_uri(conn->request_info.uri)) { + snprintf(ebuf, sizeof(ebuf), "Invalid URI: [%s]", ri->uri); diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/Testing/Issue32/Cpp/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Testing/Issue32/Cpp/CMakeLists.txt Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 2.8) + +project(Orthanc) + +set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../../../Resources/CMake) + +include(${ORTHANC_ROOT}/OrthancFrameworkParameters.cmake) +set(ENABLE_WEB_CLIENT ON) +include(${ORTHANC_ROOT}/OrthancFrameworkConfiguration.cmake) + +add_definitions( + -DORTHANC_ENABLE_LOGGING_PLUGIN=OFF + ) + +include_directories(${ORTHANC_ROOT}) + +add_executable(Sample + main.cpp + ${ORTHANC_CORE_SOURCES} + ) \ No newline at end of file diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/Testing/Issue32/Cpp/main.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Testing/Issue32/Cpp/main.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,86 @@ +#include +#include +#include +#include + +#include +#include + +static void Worker(bool *done) +{ + LOG(WARNING) << "One thread has started"; + + Orthanc::HttpClient client; + //client.SetUrl("http://localhost:8042/studies"); + //client.SetUrl("http://localhost:8042/tools/default-encoding"); + client.SetUrl("http://localhost:8042/system"); + //client.SetUrl("http://localhost:8042/"); + //client.SetCredentials("orthanc", "orthanc"); + client.SetRedirectionFollowed(false); + + while (!(*done)) + { + try + { +#if 0 + Json::Value v; + if (!client.Apply(v) || + v.type() != Json::objectValue) + { + printf("ERROR\n"); + } +#else + std::string s; + if (!client.Apply(s) || + s.empty()) + { + printf("ERROR\n"); + } +#endif + } + catch (Orthanc::OrthancException& e) + { + printf("EXCEPTION: %s", e.What()); + } + } + + LOG(WARNING) << "One thread has stopped"; +} + +int main() +{ + Orthanc::Logging::Initialize(); + //Orthanc::Logging::EnableInfoLevel(true); + Orthanc::HttpClient::GlobalInitialize(); + + { + bool done = false; + + std::vector threads; + + for (size_t i = 0; i < 100; i++) + { + threads.push_back(new boost::thread(Worker, &done)); + } + + LOG(WARNING) << "STARTED"; + Orthanc::SystemToolbox::ServerBarrier(); + LOG(WARNING) << "STOPPING"; + + done = true; + + for (size_t i = 0; i < threads.size(); i++) + { + if (threads[i]->joinable()) + { + threads[i]->join(); + } + + delete threads[i]; + } + } + + Orthanc::HttpClient::GlobalFinalize(); + printf("OK\n"); + return 0; +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/Testing/Issue32/Java/README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Testing/Issue32/Java/README.txt Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,2 @@ +$ sudo apt-get install maven +$ mvn test diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/Testing/Issue32/Java/pom.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Testing/Issue32/Java/pom.xml Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,35 @@ + + + + 4.0.0 + + io.osimis + issue32 + 1.0-SNAPSHOT + + issue32 + + http://www.example.com + + + UTF-8 + 1.8 + 1.8 + + + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + + junit + junit + 4.11 + test + + + diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/Testing/Issue32/Java/src/test/java/io/osimis/AppTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Testing/Issue32/Java/src/test/java/io/osimis/AppTest.java Thu Jan 24 10:55:19 2019 +0100 @@ -0,0 +1,66 @@ +package io.osimis; + +import java.io.IOException; +import java.util.Base64; +import org.apache.http.HttpEntity; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class AppTest +{ + @Test + public void testKeepAlive() + { + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); + + CloseableHttpClient client = HttpClients + .custom() + .setConnectionManager(cm) + .setRetryHandler((HttpRequestRetryHandler) (exception, executionCount, context) -> { + System.out.println("ERROR"); + assertTrue(false); + return false; + }).build(); + + HttpRequestBase request = new HttpGet("http://localhost:8042/system"); + + // Low-level handling of HTTP basic authentication (for integration tests) + request.addHeader("Authorization", "Basic " + + Base64.getEncoder().encodeToString("alice:orthanctest".getBytes())); + + // The following call works + //HttpRequestBase request = new HttpGet("https://api.ipify.org?format=json"); + + for (int i = 0; i < 5; i++) { + System.out.println("================================"); + try (CloseableHttpResponse httpResponse = client.execute(request)) { + String responseContent = null; + + HttpEntity entity = httpResponse.getEntity(); + if (entity != null) { + responseContent = EntityUtils.toString(entity); + } + + System.out.println(httpResponse.getStatusLine().getStatusCode()); + System.out.println(responseContent); + + EntityUtils.consume(entity); + httpResponse.close(); + } catch (IOException e) { + System.out.println("Request error " + e); + } + } + + assertTrue(true); + } +} diff -r 4cfed5c2eacd -r fc9a4a2dad63 Resources/WebAssembly/ArithmeticTests/CMakeLists.txt --- a/Resources/WebAssembly/ArithmeticTests/CMakeLists.txt Thu Jan 24 10:54:47 2019 +0100 +++ b/Resources/WebAssembly/ArithmeticTests/CMakeLists.txt Thu Jan 24 10:55:19 2019 +0100 @@ -31,7 +31,7 @@ include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake) set(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.2) -set(DCMTK_URL "http://www.orthanc-server.com/downloads/third-party/dcmtk-3.6.2.tar.gz") +set(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.2.tar.gz") set(DCMTK_MD5 "d219a4152772985191c9b89d75302d12") if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}") diff -r 4cfed5c2eacd -r fc9a4a2dad63 TODO --- a/TODO Thu Jan 24 10:54:47 2019 +0100 +++ b/TODO Thu Jan 24 10:55:19 2019 +0100 @@ -95,6 +95,8 @@ * Add plugins for normalized operations (notably so as to support Print SCU/SCP): https://www.medicalconnections.co.uk/kb/DICOM_Print_Service +* Provide access to the Orthanc::DicomUserConnection class in plugins: + https://groups.google.com/d/msg/orthanc-users/ycDA1xPuTRY/nsT2_GOtEgAJ ---------------- Ideas of plugins diff -r 4cfed5c2eacd -r fc9a4a2dad63 UnitTestsSources/DatabaseLookupTests.cpp --- a/UnitTestsSources/DatabaseLookupTests.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/UnitTestsSources/DatabaseLookupTests.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -44,28 +44,19 @@ { { ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, - "HEL*LO", true), OrthancException); + "HEL*LO", true, true), OrthancException); ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, - "HEL?LO", true), OrthancException); + "HEL?LO", true, true), OrthancException); ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, - true), OrthancException); + true, true), OrthancException); - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELLO", true); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELLO", true, 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); @@ -77,7 +68,7 @@ } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELlo", false); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELlo", false, true); ASSERT_TRUE(tag.IsMatch("HELLO")); ASSERT_TRUE(tag.IsMatch("hello")); @@ -85,7 +76,7 @@ } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*L?O", true); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*L?O", true, true); ASSERT_TRUE(tag.IsMatch("HELLO")); ASSERT_TRUE(tag.IsMatch("HELLLLLO")); ASSERT_TRUE(tag.IsMatch("HELxO")); @@ -93,7 +84,7 @@ } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*l?o", false); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*l?o", false, true); ASSERT_TRUE(tag.IsMatch("HELLO")); ASSERT_TRUE(tag.IsMatch("HELLLLLO")); ASSERT_TRUE(tag.IsMatch("HELxO")); @@ -104,21 +95,23 @@ } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_SmallerOrEqual, "123", true); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_SmallerOrEqual, "123", true, true); ASSERT_TRUE(tag.IsMatch("120")); ASSERT_TRUE(tag.IsMatch("123")); ASSERT_FALSE(tag.IsMatch("124")); + ASSERT_TRUE(tag.IsMandatory()); } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_GreaterOrEqual, "123", true); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_GreaterOrEqual, "123", true, false); ASSERT_FALSE(tag.IsMatch("122")); ASSERT_TRUE(tag.IsMatch("123")); ASSERT_TRUE(tag.IsMatch("124")); + ASSERT_FALSE(tag.IsMandatory()); } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, true); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, true, true); ASSERT_FALSE(tag.IsMatch("CT")); ASSERT_FALSE(tag.IsMatch("MR")); @@ -137,7 +130,7 @@ } { - DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, false); + DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, false, true); tag.AddValue("ct"); tag.AddValue("mr"); @@ -155,20 +148,16 @@ { { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true, 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); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); // This is *not* a PN VR => "false" above is *not* used @@ -177,14 +166,14 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", true); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", true, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive()); } { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", false); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); // This is a PN VR => "false" above is used @@ -193,11 +182,7 @@ { 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()); + lookup.AddDicomConstraint(DICOM_TAG_SERIES_DESCRIPTION, "2012-2016", false, true); // This is not a data VR ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType()); @@ -205,7 +190,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-2016", false); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-2016", false, true); // This is a data VR => range is effective ASSERT_EQ(2u, lookup.GetConstraintsCount()); @@ -221,7 +206,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(ConstraintType_GreaterOrEqual, lookup.GetConstraint(0).GetConstraintType()); @@ -230,7 +215,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "-2016", false); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "-2016", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(DICOM_TAG_PATIENT_BIRTH_DATE, lookup.GetConstraint(0).GetTag()); @@ -240,11 +225,10 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_MODALITIES_IN_STUDY, "CT\\MR", false); + lookup.AddDicomConstraint(DICOM_TAG_MODALITIES_IN_STUDY, "CT\\MR", false, true); 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(); @@ -256,11 +240,10 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "CT\\MR", false); + lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "CT\\MR", false, true); 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(); @@ -272,7 +255,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false); + lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType()); @@ -280,7 +263,7 @@ { DatabaseLookup lookup; - lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false); + lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false, true); ASSERT_EQ(1u, lookup.GetConstraintsCount()); ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType()); @@ -288,10 +271,9 @@ { 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()); + lookup.AddDicomConstraint(DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID, "TEST", false, true); + lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "TEST2", false, false); + ASSERT_TRUE(lookup.GetConstraint(0).IsMandatory()); + ASSERT_FALSE(lookup.GetConstraint(1).IsMandatory()); } } diff -r 4cfed5c2eacd -r fc9a4a2dad63 UnitTestsSources/FromDcmtkTests.cpp --- a/UnitTestsSources/FromDcmtkTests.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/UnitTestsSources/FromDcmtkTests.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -34,21 +34,22 @@ #include "PrecompiledHeadersUnitTests.h" #include "gtest/gtest.h" -#include "../Core/DicomParsing/FromDcmtkBridge.h" -#include "../Core/DicomParsing/ToDcmtkBridge.h" +#include "../Core/DicomNetworking/DicomFindAnswers.h" #include "../Core/DicomParsing/DicomModification.h" -#include "../OrthancServer/ServerToolbox.h" -#include "../Core/OrthancException.h" +#include "../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Core/DicomParsing/Internals/DicomImageDecoder.h" +#include "../Core/DicomParsing/ToDcmtkBridge.h" +#include "../Core/Endianness.h" +#include "../Core/Images/Image.h" #include "../Core/Images/ImageBuffer.h" +#include "../Core/Images/ImageProcessing.h" #include "../Core/Images/PngReader.h" #include "../Core/Images/PngWriter.h" -#include "../Core/Images/Image.h" -#include "../Core/Images/ImageProcessing.h" -#include "../Core/Endianness.h" +#include "../Core/OrthancException.h" +#include "../Core/SystemToolbox.h" +#include "../OrthancServer/ServerToolbox.h" +#include "../Plugins/Engine/PluginsEnumerations.h" #include "../Resources/EncodingTests.h" -#include "../Core/DicomNetworking/DicomFindAnswers.h" -#include "../Core/DicomParsing/Internals/DicomImageDecoder.h" -#include "../Plugins/Engine/PluginsEnumerations.h" #include #include diff -r 4cfed5c2eacd -r fc9a4a2dad63 UnitTestsSources/MultiThreadingTests.cpp --- a/UnitTestsSources/MultiThreadingTests.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/UnitTestsSources/MultiThreadingTests.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -42,7 +42,7 @@ #include "../Core/SerializationToolbox.h" #include "../Core/SystemToolbox.h" #include "../Core/Toolbox.h" -#include "../OrthancServer/DatabaseWrapper.h" +#include "../OrthancServer/Database/SQLiteDatabaseWrapper.h" #include "../OrthancServer/ServerContext.h" #include "../OrthancServer/ServerJobs/LuaJobManager.h" #include "../OrthancServer/ServerJobs/OrthancJobUnserializer.h" @@ -1281,7 +1281,7 @@ { private: MemoryStorageArea storage_; - DatabaseWrapper db_; // The SQLite DB is in memory + SQLiteDatabaseWrapper db_; // The SQLite DB is in memory std::auto_ptr context_; TimeoutDicomConnectionManager manager_; diff -r 4cfed5c2eacd -r fc9a4a2dad63 UnitTestsSources/ServerIndexTests.cpp --- a/UnitTestsSources/ServerIndexTests.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/UnitTestsSources/ServerIndexTests.cpp Thu Jan 24 10:55:19 2019 +0100 @@ -37,10 +37,9 @@ #include "../Core/FileStorage/FilesystemStorage.h" #include "../Core/FileStorage/MemoryStorageArea.h" #include "../Core/Logging.h" -#include "../OrthancServer/DatabaseWrapper.h" -#include "../OrthancServer/Search/LookupIdentifierQuery.h" +#include "../OrthancServer/Database/SQLiteDatabaseWrapper.h" +#include "../OrthancServer/Search/DatabaseLookup.h" #include "../OrthancServer/ServerContext.h" -#include "../OrthancServer/ServerIndex.h" #include "../OrthancServer/ServerToolbox.h" #include @@ -50,12 +49,6 @@ namespace { - enum DatabaseWrapperClass - { - DatabaseWrapperClass_SQLite - }; - - class TestDatabaseListener : public IDatabaseListener { public: @@ -96,34 +89,24 @@ << EnumerationToString(change.GetResourceType()) << ": " << EnumerationToString(change.GetChangeType()); } - }; - class DatabaseWrapperTest : public ::testing::TestWithParam + class DatabaseWrapperTest : public ::testing::Test { protected: - std::auto_ptr listener_; - std::auto_ptr index_; + std::auto_ptr listener_; + std::auto_ptr index_; + public: DatabaseWrapperTest() { } - virtual void SetUp() ORTHANC_OVERRIDE + virtual void SetUp() ORTHANC_OVERRIDE { listener_.reset(new TestDatabaseListener); - - switch (GetParam()) - { - case DatabaseWrapperClass_SQLite: - index_.reset(new DatabaseWrapper()); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - + index_.reset(new SQLiteDatabaseWrapper); index_->SetListener(*listener_); index_->Open(); } @@ -137,94 +120,35 @@ void CheckTableRecordCount(uint32_t expected, const char* table) { - switch (GetParam()) - { - case DatabaseWrapperClass_SQLite: - { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); - ASSERT_EQ(expected, sqlite->GetTableRecordCount(table)); - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } + ASSERT_EQ(expected, index_->GetTableRecordCount(table)); } void CheckNoParent(int64_t id) { std::string s; - - switch (GetParam()) - { - case DatabaseWrapperClass_SQLite: - { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); - ASSERT_FALSE(sqlite->GetParentPublicId(s, id)); - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } + ASSERT_FALSE(index_->GetParentPublicId(s, id)); } void CheckParentPublicId(const char* expected, int64_t id) { std::string s; - - switch (GetParam()) - { - case DatabaseWrapperClass_SQLite: - { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); - ASSERT_TRUE(sqlite->GetParentPublicId(s, id)); - ASSERT_EQ(expected, s); - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } + ASSERT_TRUE(index_->GetParentPublicId(s, id)); + ASSERT_EQ(expected, s); } void CheckNoChild(int64_t id) { std::list j; - - switch (GetParam()) - { - case DatabaseWrapperClass_SQLite: - { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); - sqlite->GetChildren(j, id); - ASSERT_EQ(0u, j.size()); - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } + index_->GetChildren(j, id); + ASSERT_EQ(0u, j.size()); } void CheckOneChild(const char* expected, int64_t id) { std::list j; - - switch (GetParam()) - { - case DatabaseWrapperClass_SQLite: - { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); - sqlite->GetChildren(j, id); - ASSERT_EQ(1u, j.size()); - ASSERT_EQ(expected, j.front()); - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } + index_->GetChildren(j, id); + ASSERT_EQ(1u, j.size()); + ASSERT_EQ(expected, j.front()); } void CheckTwoChildren(const char* expected1, @@ -232,45 +156,52 @@ int64_t id) { std::list j; - - switch (GetParam()) - { - case DatabaseWrapperClass_SQLite: - { - DatabaseWrapper* sqlite = dynamic_cast(index_.get()); - sqlite->GetChildren(j, id); - ASSERT_EQ(2u, j.size()); - ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) || - (expected1 == j.back() && expected2 == j.front())); - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } + index_->GetChildren(j, id); + ASSERT_EQ(2u, j.size()); + ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) || + (expected1 == j.back() && expected2 == j.front())); } - - void DoLookup(std::list& result, - ResourceType level, - const DicomTag& tag, - const std::string& value) + void DoLookupIdentifier(std::list& result, + ResourceType level, + const DicomTag& tag, + ConstraintType type, + const std::string& value) { - LookupIdentifierQuery query(level); - query.AddConstraint(tag, IdentifierConstraintType_Equal, value); - query.Apply(result, *index_); + assert(ServerToolbox::IsIdentifier(tag, level)); + + DicomTagConstraint c(tag, type, value, true, true); + + std::vector lookup; + lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + + index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */); + } + + void DoLookupIdentifier2(std::list& result, + ResourceType level, + const DicomTag& tag, + ConstraintType type1, + const std::string& value1, + ConstraintType type2, + const std::string& value2) + { + assert(ServerToolbox::IsIdentifier(tag, level)); + + DicomTagConstraint c1(tag, type1, value1, true, true); + DicomTagConstraint c2(tag, type2, value2, true, true); + + std::vector lookup; + lookup.push_back(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + lookup.push_back(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier)); + + index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */); } - }; } -INSTANTIATE_TEST_CASE_P(DatabaseWrapperName, - DatabaseWrapperTest, - ::testing::Values(DatabaseWrapperClass_SQLite)); - - -TEST_P(DatabaseWrapperTest, Simple) +TEST_F(DatabaseWrapperTest, Simple) { int64_t a[] = { index_->CreateResource("a", ResourceType_Patient), // 0 @@ -464,7 +395,15 @@ CheckTableRecordCount(0, "Resources"); CheckTableRecordCount(0, "AttachedFiles"); - CheckTableRecordCount(2, "GlobalProperties"); + CheckTableRecordCount(3, "GlobalProperties"); + + std::string tmp; + ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion)); + ASSERT_EQ("6", tmp); + ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep)); + ASSERT_EQ("World", tmp); + ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast)); + ASSERT_EQ("1", tmp); ASSERT_EQ(3u, listener_->deletedFiles_.size()); ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), @@ -473,9 +412,7 @@ } - - -TEST_P(DatabaseWrapperTest, Upward) +TEST_F(DatabaseWrapperTest, Upward) { int64_t a[] = { index_->CreateResource("a", ResourceType_Patient), // 0 @@ -526,7 +463,7 @@ } -TEST_P(DatabaseWrapperTest, PatientRecycling) +TEST_F(DatabaseWrapperTest, PatientRecycling) { std::vector patients; for (int i = 0; i < 10; i++) @@ -587,7 +524,7 @@ } -TEST_P(DatabaseWrapperTest, PatientProtection) +TEST_F(DatabaseWrapperTest, PatientProtection) { std::vector patients; for (int i = 0; i < 5; i++) @@ -669,14 +606,13 @@ } - TEST(ServerIndex, Sequence) { const std::string path = "UnitTestsStorage"; SystemToolbox::RemoveFile(path + "/index"); FilesystemStorage storage(path); - DatabaseWrapper db; // The SQLite DB is in memory + SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10); context.SetupJobsEngine(true, false); @@ -693,8 +629,7 @@ } - -TEST_P(DatabaseWrapperTest, LookupIdentifier) +TEST_F(DatabaseWrapperTest, LookupIdentifier) { int64_t a[] = { index_->CreateResource("a", ResourceType_Study), // 0 @@ -710,73 +645,56 @@ std::list s; - DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "0"); + DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "0"); ASSERT_EQ(2u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), "a") != s.end()); ASSERT_TRUE(std::find(s.begin(), s.end(), "c") != s.end()); - DoLookup(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, "0"); + DoLookupIdentifier(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, ConstraintType_Equal, "0"); ASSERT_EQ(1u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), "d") != s.end()); - DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "1"); + DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "1"); ASSERT_EQ(1u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end()); - DoLookup(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, "1"); + DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "1"); ASSERT_EQ(1u, s.size()); ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end()); - DoLookup(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, "1"); + DoLookupIdentifier(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, ConstraintType_Equal, "1"); + ASSERT_EQ(0u, s.size()); + + DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "0"); + ASSERT_EQ(3u, s.size()); + + DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "1"); + ASSERT_EQ(1u, s.size()); + + DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "2"); ASSERT_EQ(0u, s.size()); - { - LookupIdentifierQuery query(ResourceType_Study); - query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "0"); - query.Apply(s, *index_); - ASSERT_EQ(3u, s.size()); - } - - { - LookupIdentifierQuery query(ResourceType_Study); - query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "0"); - query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_SmallerOrEqual, "0"); - query.Apply(s, *index_); - ASSERT_EQ(2u, s.size()); - } + DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, + ConstraintType_GreaterOrEqual, "0", ConstraintType_SmallerOrEqual, "0"); + ASSERT_EQ(2u, s.size()); - { - LookupIdentifierQuery query(ResourceType_Study); - query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "1"); - query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_SmallerOrEqual, "1"); - query.Apply(s, *index_); - ASSERT_EQ(1u, s.size()); - } + DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, + ConstraintType_GreaterOrEqual, "1", ConstraintType_SmallerOrEqual, "1"); + ASSERT_EQ(1u, s.size()); - { - LookupIdentifierQuery query(ResourceType_Study); - query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "1"); - query.Apply(s, *index_); - ASSERT_EQ(1u, s.size()); - } - - { - LookupIdentifierQuery query(ResourceType_Study); - query.AddConstraint(DICOM_TAG_STUDY_INSTANCE_UID, IdentifierConstraintType_GreaterOrEqual, "2"); - query.Apply(s, *index_); - ASSERT_EQ(0u, s.size()); - } + DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, + ConstraintType_GreaterOrEqual, "0", ConstraintType_SmallerOrEqual, "1"); + ASSERT_EQ(3u, s.size()); } - TEST(ServerIndex, AttachmentRecycling) { const std::string path = "UnitTestsStorage"; SystemToolbox::RemoveFile(path + "/index"); FilesystemStorage storage(path); - DatabaseWrapper db; // The SQLite DB is in memory + SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10); context.SetupJobsEngine(true, false); @@ -850,7 +768,7 @@ } -TEST(LookupIdentifierQuery, NormalizeIdentifier) +TEST(ServerIndex, NormalizeIdentifier) { ASSERT_EQ("H^L.LO", ServerToolbox::NormalizeIdentifier(" Hé^l.LO %_ ")); ASSERT_EQ("1.2.840.113619.2.176.2025", ServerToolbox::NormalizeIdentifier(" 1.2.840.113619.2.176.2025 ")); @@ -864,7 +782,7 @@ bool overwrite = (i == 0); MemoryStorageArea storage; - DatabaseWrapper db; // The SQLite DB is in memory + SQLiteDatabaseWrapper db; // The SQLite DB is in memory db.Open(); ServerContext context(db, storage, true /* running unit tests */, 10); context.SetupJobsEngine(true, false); diff -r 4cfed5c2eacd -r fc9a4a2dad63 UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Thu Jan 24 10:54:47 2019 +0100 +++ b/UnitTestsSources/UnitTestsMain.cpp Thu Jan 24 10:55:19 2019 +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__)