changeset 3160:fc9a4a2dad63

merge
author Alain Mazy <alain@mazy.be>
date Thu, 24 Jan 2019 10:55:19 +0100
parents 4cfed5c2eacd (current diff) b6e7714c3fe6 (diff)
children 5cf29046c159
files Core/HttpServer/MongooseServer.cpp Core/HttpServer/MongooseServer.h OrthancExplorer/file-upload.js OrthancServer/DatabaseWrapper.cpp OrthancServer/DatabaseWrapper.h OrthancServer/IDatabaseListener.h OrthancServer/IDatabaseWrapper.h OrthancServer/PrepareDatabase.sql OrthancServer/Search/IFindConstraint.cpp OrthancServer/Search/IFindConstraint.h OrthancServer/Search/ListConstraint.cpp OrthancServer/Search/ListConstraint.h OrthancServer/Search/LookupIdentifierQuery.cpp OrthancServer/Search/LookupIdentifierQuery.h OrthancServer/Search/LookupResource.cpp OrthancServer/Search/LookupResource.h OrthancServer/Search/RangeConstraint.cpp OrthancServer/Search/RangeConstraint.h OrthancServer/Search/SetOfResources.cpp OrthancServer/Search/SetOfResources.h OrthancServer/Search/ValueConstraint.cpp OrthancServer/Search/ValueConstraint.h OrthancServer/Search/WildcardConstraint.cpp OrthancServer/Search/WildcardConstraint.h OrthancServer/Upgrade3To4.sql OrthancServer/Upgrade4To5.sql
diffstat 176 files changed, 10012 insertions(+), 6894 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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
--- 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<unsigned int>(sinceMidnight.seconds());  // seconds after the minute - [0,59]
+  zfi.tmz_date.tm_min = static_cast<unsigned int>(sinceMidnight.minutes());  // minutes after the hour - [0,59]
+  zfi.tmz_date.tm_hour = static_cast<unsigned int>(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]
--- 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<DcmDataset> 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<DcmElement*>(element->clone()));
+      }
+    }
+    
+    return target.release();
   }
 
 
--- 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,
--- 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 */
--- 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;
--- 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)
             {
--- 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<unsigned int>(timeout_.total_milliseconds());
   }
 
 
--- 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;
 
--- 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");
       }
 
--- 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)
--- 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);
--- 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<int>(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;
--- 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<std::string>::const_iterator
            it = headers_.begin(); it != headers_.end(); ++it)
--- /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://www.gnu.org/licenses/>.
+ **/
+
+
+// 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 <mongoose.h>
+
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+#  include <civetweb.h>
+#  define MONGOOSE_USE_CALLBACKS 1
+
+#else
+#  error "Either Mongoose or Civetweb must be enabled to compile this file"
+#endif
+
+#include <algorithm>
+#include <string.h>
+#include <boost/lexical_cast.hpp>
+#include <boost/algorithm/string.hpp>
+#include <iostream>
+#include <string.h>
+#include <stdio.h>
+#include <boost/thread.hpp>
+
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error The macro ORTHANC_ENABLE_SSL must be defined
+#endif
+
+#if ORTHANC_ENABLE_SSL == 1
+#include <openssl/opensslv.h>
+#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<int>(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<ChunkedFile*>  Content;
+    Content  content_;
+    unsigned int numPlaces_;
+
+    boost::mutex mutex_;
+    std::set<std::string> 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<std::string>::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<int>(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<size_t>(fileSizeStr->second);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return PostDataStatus_Failure;
+      }
+    }
+
+    typedef boost::find_iterator<std::string::iterator> FindIterator;
+    typedef boost::iterator_range<char*> 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<std::string> 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<const uint8_t*>(&request->remote_ip) [3], 
+            reinterpret_cast<const uint8_t*>(&request->remote_ip) [2], 
+            reinterpret_cast<const uint8_t*>(&request->remote_ip) [1], 
+            reinterpret_cast<const uint8_t*>(&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<HttpServer*>(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<std::string>(port_);
+      std::string numThreads = boost::lexical_cast<std::string>(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");
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <list>
+#include <map>
+#include <set>
+#include <stdint.h>
+#include <boost/shared_ptr.hpp>
+
+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> pimpl_;
+
+    IHttpHandler *handler_;
+
+    typedef std::set<std::string> 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_;
+    }
+  };
+}
--- 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;
   };
 }
--- 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://www.gnu.org/licenses/>.
- **/
-
-
-// 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 <mongoose.h>
-
-#elif ORTHANC_ENABLE_CIVETWEB == 1
-#  include <civetweb.h>
-#  define MONGOOSE_USE_CALLBACKS 1
-
-#else
-#  error "Either Mongoose or Civetweb must be enabled to compile this file"
-#endif
-
-#include <algorithm>
-#include <string.h>
-#include <boost/lexical_cast.hpp>
-#include <boost/algorithm/string.hpp>
-#include <iostream>
-#include <string.h>
-#include <stdio.h>
-#include <boost/thread.hpp>
-
-#if !defined(ORTHANC_ENABLE_SSL)
-#  error The macro ORTHANC_ENABLE_SSL must be defined
-#endif
-
-#if ORTHANC_ENABLE_SSL == 1
-#include <openssl/opensslv.h>
-#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<int>(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<ChunkedFile*>  Content;
-    Content  content_;
-    unsigned int numPlaces_;
-
-    boost::mutex mutex_;
-    std::set<std::string> 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<std::string>::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<int>(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<size_t>(fileSizeStr->second);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        return PostDataStatus_Failure;
-      }
-    }
-
-    typedef boost::find_iterator<std::string::iterator> FindIterator;
-    typedef boost::iterator_range<char*> 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<std::string> 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<const uint8_t*>(&request->remote_ip) [3], 
-            reinterpret_cast<const uint8_t*>(&request->remote_ip) [2], 
-            reinterpret_cast<const uint8_t*>(&request->remote_ip) [1], 
-            reinterpret_cast<const uint8_t*>(&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<MongooseServer*>(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<std::string>(port_);
-      std::string numThreads = boost::lexical_cast<std::string>(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;
-  }
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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 <list>
-#include <map>
-#include <set>
-#include <stdint.h>
-#include <boost/shared_ptr.hpp>
-
-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> pimpl_;
-
-    IHttpHandler *handler_;
-
-    typedef std::set<std::string> 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_;
-    }
-  };
-}
--- 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);
   };
 }
--- 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;
       }
--- 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 \
--- 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
--- 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 @@
     </div>
     <div data-role="content">
       <div>
-        <!-- It's very difficult to style a "file" input so we actually hide it and create a "proxy" button that is forwarding its click to the "file" input -->
+        <!-- It's very difficult to style a "file" input so we
+        actually hide it and create a "proxy" button that is
+        forwarding its click to the "file" input -->
         <input id="fileupload" type="file" name="files[]" data-url="../instances/" style="display:none" multiple>
-        <ul data-role="listview" data-inset="true">
-          <li id="fileupload-proxy" onclick="$('#fileupload').click()" data-icon="arrow-r" data-theme="d"><a href="#">Select
-              files to upload ...</a></li>
-        </ul>
       </div>
       <p>
         <ul data-role="listview" data-inset="true">
+          <li id="fileupload-proxy" onclick="$('#fileupload').click()" data-icon="arrow-r" data-theme="e">
+            <a href="#">Select files to upload ...</a>
+          </li>
           <li data-icon="arrow-r" data-theme="e"><a href="#" id="upload-button">Start the upload</a></li>
           <!--li data-icon="gear" data-theme="e"><a href="#" id="upload-abort" class="ui-disabled">Abort the current upload</a></li-->
-          <li data-icon="delete" data-theme="e"><a href="#" id="upload-clear">Clear the pending uploads</a></li>
+          <li data-icon="delete" data-theme="d"><a href="#" id="upload-clear">Clear the pending uploads</a></li>
         </ul>
         <div id="progress" class="ui-corner-all">
           <span class="bar ui-corner-all"></span>
           <div class="label"></div>
         </div>
       </p>
+      <div class="ui-bar ui-bar-e" id="issue-21-warning">
+        <h3>Warning:</h3> 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.
+      </div>
       <ul id="upload-list" data-role="listview" data-inset="true">
         <li data-role="list-divider">Drag and drop DICOM files here</li>
       </ul>
-      <div class="ui-bar ui-bar-e" id="issue-21-warning">
-        <h3>Warning:</h3> 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 !
-      </div>
     </div>
   </div>
 
@@ -636,4 +639,4 @@
   </div>
 </body>
 
-</html>
\ No newline at end of file
+</html>
--- 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;
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<DicomTagConstraint*>  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<int64_t> 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<int64_t> 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<int64_t> 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<DatabaseConstraint>& lookup,
+                           ResourceType level)
+    {
+      typedef std::set<const DatabaseConstraint*>  SetOfConstraints;
+      typedef std::map<DicomTag, SetOfConstraints> 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<int64_t>  source;
+        candidates.Flatten(compatibility, source);
+        candidates.Clear();
+
+        std::list<int64_t>  filtered;
+        for (std::list<int64_t>::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<ResourceType>(i));
+
+        std::list<int64_t> children;
+        compatibility.GetChildrenInternalId(children, resource);
+          
+        if (children.empty())
+        {
+          throw OrthancException(ErrorCode_Database);
+        }
+          
+        resource = children.front();
+      }
+
+      return compatibility.GetPublicId(resource);
+    }
+                           
+
+    void DatabaseLookup::ApplyLookupResources(std::list<std::string>& resourcesId,
+                                              std::list<std::string>* instancesId,
+                                              const std::vector<DatabaseConstraint>& 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<ResourceType>(level));
+
+        if (level != lowerLevel)
+        {
+          candidates.GoDown();
+        }
+      }
+
+      std::list<int64_t> resources;
+      candidates.Flatten(compatibility_, resources);
+
+      // Climb up, up to queryLevel
+
+      for (int level = lowerLevel; level > queryLevel; level--)
+      {
+        std::list<int64_t> parents;
+        for (std::list<int64_t>::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<int64_t>::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));
+          }
+        }
+      }
+    }
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<std::string>& resourcesId,
+                                std::list<std::string>* instancesId,
+                                const std::vector<DatabaseConstraint>& lookup,
+                                ResourceType queryLevel,
+                                size_t limit);
+    };
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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;
+    }
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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);
+    };
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeadersServer.h"
+#include "IGetChildrenMetadata.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    void IGetChildrenMetadata::Apply(IGetChildrenMetadata& database,
+                                     std::list<std::string>& 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<int64_t> children;
+      database.GetChildrenInternalId(children, resourceId);
+
+      for (std::list<int64_t>::const_iterator 
+             it = children.begin(); it != children.end(); ++it)
+      {
+        std::string value;
+        if (database.LookupMetadata(value, *it, metadata))
+        {
+          target.push_back(value);
+        }
+      }
+    }
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../ServerEnumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <list>
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class IGetChildrenMetadata : public boost::noncopyable
+    {
+    public:
+      virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                         int64_t id) = 0;
+
+      virtual bool LookupMetadata(std::string& target,
+                                  int64_t id,
+                                  MetadataType type) = 0;
+
+      static void Apply(IGetChildrenMetadata& database,
+                        std::list<std::string>& target,
+                        int64_t resourceId,
+                        MetadataType metadata);
+    };
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeadersServer.h"
+#include "ILookupResources.h"
+
+#include "DatabaseLookup.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    void ILookupResources::Apply(
+      IDatabaseWrapper& database,
+      ILookupResources& compatibility,
+      std::list<std::string>& resourcesId,
+      std::list<std::string>* instancesId,
+      const std::vector<DatabaseConstraint>& lookup,
+      ResourceType queryLevel,
+      size_t limit)
+    {
+      Compatibility::DatabaseLookup compat(database, compatibility);
+      compat.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit);
+    }
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<int64_t>& target,
+                                     ResourceType resourceType) = 0;
+      
+      virtual void LookupIdentifier(std::list<int64_t>& result,
+                                    ResourceType level,
+                                    const DicomTag& tag,
+                                    IdentifierConstraintType type,
+                                    const std::string& value) = 0;
+ 
+      virtual void LookupIdentifierRange(std::list<int64_t>& result,
+                                         ResourceType level,
+                                         const DicomTag& tag,
+                                         const std::string& start,
+                                         const std::string& end) = 0;
+
+      static void Apply(IDatabaseWrapper& database,
+                        ILookupResources& compatibility,
+                        std::list<std::string>& resourcesId,
+                        std::list<std::string>* instancesId,
+                        const std::vector<DatabaseConstraint>& lookup,
+                        ResourceType queryLevel,
+                        size_t limit);
+    };
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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);
+      }
+    };
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeadersServer.h"
+#include "SetOfResources.h"
+
+#include "../../../Core/OrthancException.h"
+
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    void SetOfResources::Intersect(const std::list<int64_t>& resources)
+    {
+      if (resources_.get() == NULL)
+      {
+        resources_.reset(new Resources);
+
+        for (std::list<int64_t>::const_iterator
+               it = resources.begin(); it != resources.end(); ++it)
+        {
+          resources_->insert(*it);
+        }
+      }
+      else
+      {
+        std::auto_ptr<Resources> filtered(new Resources);
+
+        for (std::list<int64_t>::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<Resources> children(new Resources);
+
+        for (Resources::const_iterator it = resources_->begin(); 
+             it != resources_->end(); ++it)
+        {
+          std::list<int64_t> tmp;
+          database_.GetChildrenInternalId(tmp, *it);
+
+          for (std::list<int64_t>::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<std::string>& 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<int64_t>& 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);
+        }
+      }
+    }
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../IDatabaseWrapper.h"
+#include "ILookupResources.h"
+
+#include <set>
+#include <memory>
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class SetOfResources : public boost::noncopyable
+    {
+    private:
+      typedef std::set<int64_t>  Resources;
+
+      IDatabaseWrapper&         database_;
+      ResourceType              level_;
+      std::auto_ptr<Resources>  resources_;
+    
+    public:
+      SetOfResources(IDatabaseWrapper& database,
+                     ResourceType level) : 
+        database_(database),
+        level_(level)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      void Intersect(const std::list<int64_t>& resources);
+
+      void GoDown();
+
+      void Flatten(ILookupResources& compatibility,
+                   std::list<int64_t>& result);
+
+      void Flatten(std::list<std::string>& result);
+
+      void Clear()
+      {
+        resources_.reset(NULL);
+      }
+    };
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../ServerEnumerations.h"
+#include "../ServerIndexChange.h"
+
+#include <string>
+
+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;
+  };
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <list>
+#include <boost/noncopyable.hpp>
+
+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<MetadataType, std::string>& target,
+                                int64_t id) = 0;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType) = 0;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit) = 0;
+
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults) = 0;
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id) = 0;
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id) = 0;
+
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults) = 0;
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& 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<MetadataType>& target,
+                                       int64_t id) = 0;
+
+    virtual void ListAvailableAttachments(std::list<FileContentType>& 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<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId, // Can be NULL if not needed
+                                      const std::vector<DatabaseConstraint>& 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<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata) = 0;
+
+    virtual int64_t GetLastChangeIndex() = 0;
+  };
+}
--- /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;
--- /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");
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "ResourcesContent.h"
+
+#include "Compatibility/ISetResourcesContent.h"
+
+#include <cassert>
+
+
+namespace Orthanc
+{
+  void ResourcesContent::Store(Compatibility::ISetResourcesContent& compatibility) const
+  {
+    for (std::list<TagValue>::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<Metadata>::const_iterator
+           it = metadata_.begin(); it != metadata_.end(); ++it)
+    {
+      compatibility.SetMetadata(it->resourceId_, it->metadata_,  it->value_);
+    }
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/DicomFormat/DicomMap.h"
+#include "../ServerEnumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <list>
+
+
+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<TagValue>  ListTags;
+    typedef std::list<Metadata>  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_;
+    }
+  };
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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 <EmbeddedResources.h>
+
+#include <stdio.h>
+#include <boost/lexical_cast.hpp>
+
+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<FileContentType>(context.GetIntValue(1)),
+                      static_cast<uint64_t>(context.GetInt64Value(2)),
+                      uncompressedMD5,
+                      static_cast<CompressionType>(context.GetIntValue(3)),
+                      static_cast<uint64_t>(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<ResourceType>(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<ResourceType>(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<ServerIndexChange>& 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<ChangeType>(s.ColumnInt(1));
+      ResourceType resourceType = static_cast<ResourceType>(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<ExportedResource>& 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<ResourceType>(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<std::string>& 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<int>(tmp);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "Global property " + boost::lexical_cast<std::string>(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<unsigned int>(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<std::string>(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<ResourceType>(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<ServerIndexChange>& 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<ServerIndexChange>& 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<SQLite::Transaction>  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<int64_t>(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<int64_t>(that_.GetTotalCompressedSize()));
+    }
+  };
+
+
+  IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction()
+  {
+    return new Transaction(*this);
+  }
+
+
+  void SQLiteDatabaseWrapper::GetAllMetadata(std::map<MetadataType, std::string>& 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<MetadataType>(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<ResourceType>(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<MetadataType>& 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<MetadataType>(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<FileContentType>& 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<FileContentType>(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<CompressionType>(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<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.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<int64_t>& 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<ExportedResource>& 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<ExportedResource>& 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<uint64_t>(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<uint64_t>(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<std::string>& 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<std::string>& 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<std::string>  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<std::string>(level);
+    }
+
+    virtual std::string FormatWildcardEscape()
+    {
+      return "ESCAPE '\\'";
+    }
+
+    void Bind(SQLite::Statement& statement) const
+    {
+      size_t pos = 0;
+      
+      for (std::list<std::string>::const_iterator
+             it = values_.begin(); it != values_.end(); ++it, pos++)
+      {
+        statement.BindString(pos, *it);
+      }
+    }
+  };
+
+  
+  static void AnswerLookup(std::list<std::string>& resourcesId,
+                           std::list<std::string>& instancesId,
+                           SQLite::Connection& db,
+                           ResourceType level)
+  {
+    resourcesId.clear();
+    instancesId.clear();
+    
+    std::auto_ptr<SQLite::Statement> 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<std::string>& resourcesId,
+                                                   std::list<std::string>* instancesId,
+                                                   const std::vector<DatabaseConstraint>& 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();
+    }
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<ServerIndexChange>& target,
+                            bool& done,
+                            SQLite::Statement& s,
+                            uint32_t maxResults);
+
+    void GetExportedResourcesInternal(std::list<ExportedResource>& 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<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& 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<MetadataType, std::string>& 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<std::string>& 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<MetadataType>& 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<FileContentType>& 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<std::string>& target,
+                                     int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& 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<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& 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<std::string>& target,
+                                 ResourceType resourceType)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetAllPublicIds(std::list<std::string>& 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<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId,
+                                      const std::vector<DatabaseConstraint>& 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<std::string>& 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;
+  };
+}
--- /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;
--- /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;
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "PrecompiledHeadersServer.h"
-#include "DatabaseWrapper.h"
-
-#include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/Logging.h"
-#include "EmbeddedResources.h"
-#include "ServerToolbox.h"
-
-#include <stdio.h>
-#include <boost/lexical_cast.hpp>
-
-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<FileContentType>(context.GetIntValue(1)),
-                      static_cast<uint64_t>(context.GetInt64Value(2)),
-                      uncompressedMD5,
-                      static_cast<CompressionType>(context.GetIntValue(3)),
-                      static_cast<uint64_t>(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<ResourceType>(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<ResourceType>(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<ServerIndexChange>& 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<ChangeType>(s.ColumnInt(1));
-      ResourceType resourceType = static_cast<ResourceType>(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<ExportedResource>& 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<ResourceType>(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<std::string>& 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<unsigned int>(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<std::string>(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<ResourceType>(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<ServerIndexChange>& 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<ServerIndexChange>& 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<MetadataType, std::string>& 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<MetadataType>(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<ResourceType>(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<MetadataType>& 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<MetadataType>(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<FileContentType>& 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<FileContentType>(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<CompressionType>(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<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.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<int64_t>& 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<ExportedResource>& 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<ExportedResource>& 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<uint64_t>(s.ColumnInt64(0));
-  }
-
-    
-  uint64_t DatabaseWrapper::GetTotalUncompressedSize()
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
-    s.Run();
-    return static_cast<uint64_t>(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<int64_t>& 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<std::string>& 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<std::string>& 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<int64_t>& 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<SQLite::Statement> 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<int64_t>& 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));
-    }    
-  }
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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<ServerIndexChange>& target,
-                            bool& done,
-                            SQLite::Statement& s,
-                            uint32_t maxResults);
-
-    void GetExportedResourcesInternal(std::list<ExportedResource>& 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<ServerIndexChange>& target /*out*/,
-                            bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults);
-
-    virtual void GetLastChange(std::list<ServerIndexChange>& 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<MetadataType, std::string>& 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<std::string>& 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<MetadataType>& 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<FileContentType>& 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<std::string>& target,
-                                     int64_t id);
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id);
-
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change);
-
-    virtual void LogExportedResource(const ExportedResource& resource);
-    
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults);
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/);
-
-    virtual uint64_t GetTotalCompressedSize();
-    
-    virtual uint64_t GetTotalUncompressedSize();
-
-    virtual uint64_t GetResourceCount(ResourceType resourceType);
-
-    virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                   ResourceType resourceType);
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType);
-
-    virtual void GetAllPublicIds(std::list<std::string>& 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<int64_t>& result,
-                                  ResourceType level,
-                                  const DicomTag& tag,
-                                  IdentifierConstraintType type,
-                                  const std::string& value);
-
-    virtual void LookupIdentifierRange(std::list<int64_t>& result,
-                                       ResourceType level,
-                                       const DicomTag& tag,
-                                       const std::string& start,
-                                       const std::string& end);
-  };
-}
--- 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
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include <string>
-#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;
-  };
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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 <list>
-#include <boost/noncopyable.hpp>
-
-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<MetadataType, std::string>& target,
-                                int64_t id) = 0;
-
-    virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                   ResourceType resourceType) = 0;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType) = 0;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) = 0;
-
-    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                            bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults) = 0;
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id) = 0;
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id) = 0;
-
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults) = 0;
-
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& 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<MetadataType>& target,
-                                       int64_t id) = 0;
-
-    virtual void ListAvailableAttachments(std::list<FileContentType>& 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<int64_t>& result,
-                                  ResourceType level,
-                                  const DicomTag& tag,
-                                  IdentifierConstraintType type,
-                                  const std::string& value) = 0;
-
-    virtual void LookupIdentifierRange(std::list<int64_t>& 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;
-  };
-}
--- 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();
 
--- 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<std::string>& target) const;
 
-    void SetupRegisteredUsers(MongooseServer& httpServer) const;
+    void SetupRegisteredUsers(HttpServer& httpServer) const;
 
     std::string InterpretStringParameterAsPath(const std::string& parameter) const;
     
--- 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 <boost/regex.hpp> 
@@ -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);
   }
 
 
--- 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:
--- 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 <dcmtk/dcmnet/dul.h>   // For dcmDisableGethostbyaddr()
@@ -308,7 +308,7 @@
     {
     }
 
-    return new DatabaseWrapper(indexDirectory.string() + "/index");
+    return new SQLiteDatabaseWrapper(indexDirectory.string() + "/index");
   }
 
 
--- 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
 {
--- 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<DicomModalityStoreJob>  job_;
       size_t                                position_;
+      size_t                                countInstances_;
       
     public:
       AsynchronousMove(ServerContext& context,
@@ -154,6 +157,8 @@
         std::list<std::string> tmp;
         context_.GetIndex().GetChildInstances(tmp, publicId);
 
+        countInstances_ = tmp.size();
+
         job_->Reserve(tmp.size());
 
         for (std::list<std::string>::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<std::string> ids;
+    std::vector<std::string> 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;
     }
   }
--- 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:
--- 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";
--- 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"
--- 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<std::string> tmp;
+    std::vector<std::string> tmp;
     index.LookupIdentifierExact(tmp, level, tag, value);
 
-    for (std::list<std::string>::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<size_t>(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);
     }
   }
 
--- 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"
 
 
--- 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 <boost/thread.hpp>
 
 #endif
--- 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");
--- 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
--- 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:
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<std::string>& 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<const char*>& 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    
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/DicomFormat/DicomMap.h"
+#include "../ServerEnumerations.h"
+
+#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include <orthanc/OrthancCDatabasePlugin.h>
+#  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<std::string>  values_;
+    bool                      caseSensitive_;
+    bool                      mandatory_;
+
+  public:
+    DatabaseConstraint(ResourceType level,
+                       const DicomTag& tag,
+                       bool isIdentifier,
+                       ConstraintType type,
+                       const std::vector<std::string>& 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<const char*>& tmpValues) const;
+#endif    
+  };
+}
--- 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<DicomTag, TagInfo>::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<DicomTag> ignoreTagLength;
+      std::auto_ptr<DicomValue> 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<DicomTagConstraint> constraint
+        (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive, mandatoryTag));
+
+      std::vector<std::string> 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<DicomTagConstraint> constraint
-        (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive));
-
-      std::vector<std::string> 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<DicomTag> 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;
+  }
 }
--- 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<DicomTagConstraint*>  constraints_;
 
-      TagInfo(DicomTagType type,
-              ResourceType level) :
-        type_(type),
-        level_(level)
-      {
-      }
-
-      DicomTagType GetType() const
-      {
-        return type_;
-      }
-
-      ResourceType GetLevel() const
-      {
-        return level_;
-      }
-    };
-
-    std::vector<DicomTagConstraint*>  constraints_;
-    std::map<DicomTag, TagInfo>       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;
   };
 }
--- 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 <boost/regex.hpp>
 
@@ -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<std::string>::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<std::string> values;
+    values.reserve(values_.size());
+      
+    for (std::set<std::string>::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_);
+  }  
 }
--- 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 <boost/shared_ptr.hpp>
 
@@ -46,37 +47,30 @@
     class NormalizedString;
     class RegularExpression;
 
-    bool                    hasTagInfo_;
-    DicomTagType            tagType_;
-    ResourceType            level_;
     DicomTag                tag_;
     ConstraintType          constraintType_;
     std::set<std::string>   values_;
     bool                    caseSensitive_;
+    bool                    mandatory_;
 
     boost::shared_ptr<RegularExpression>  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;
   };
 }
--- 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<DcmSequenceOfItems&>(*element);
 
@@ -127,12 +117,20 @@
       }
       else
       {
+        flatTags_.insert(tag);
+
         std::set<DicomTag> ignoreTagLength;
         std::auto_ptr<DicomValue> 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<DicomTag> 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<DicomTag>::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<DicomTag> ignoreTagLength;
-        std::auto_ptr<DicomValue> 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<DcmDataset> target(new DcmDataset);
 
-    for (Constraints::const_iterator it = constraints_.begin();
-         it != constraints_.end(); ++it)
+    for (std::set<DicomTag>::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<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(it->first));
+        std::auto_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it));
         cloned->copyFrom(*element);
         target->insert(cloned.release());
       }
--- 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<DicomTag, IFindConstraint*>      Constraints;
     typedef std::map<DicomTag, HierarchicalMatcher*>  Sequences;
 
-    Constraints  constraints_;
-    Sequences    sequences_;
+    std::set<DicomTag>  flatTags_;
+    DatabaseLookup      flatConstraints_;
+    Sequences           sequences_;
 
     void Setup(DcmItem& query,
                bool caseSensitivePN,
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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<ListConstraint> constraint(new ListConstraint(caseSensitive));
-
-      std::vector<std::string> 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);
-    }
-  }
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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);
-  };
-}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<std::string>(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<std::string>(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<std::string>(constraint.GetTag().GetGroup()) +
+               " AND " + tag + ".tagElement = " +
+               boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
+  }
+  
+
+  void ISqlLookupFormatter::Apply(std::string& sql,
+                                  ISqlLookupFormatter& formatter,
+                                  const std::vector<DatabaseConstraint>& 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<ResourceType>(level)) + " ON " +
+              FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" +
+              FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId");
+    }
+      
+    for (int level = queryLevel + 1; level <= lowerLevel; level++)
+    {
+      sql += (" INNER JOIN Resources " +
+              FormatLevel(static_cast<ResourceType>(level)) + " ON " +
+              FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
+              FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
+    }
+      
+    sql += (joins + " WHERE " + FormatLevel(queryLevel) + ".resourceType = " +
+            formatter.FormatResourceType(queryLevel) + comparisons);
+
+    if (limit != 0)
+    {
+      sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
+    }
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/Enumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <vector>
+
+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<DatabaseConstraint>& lookup,
+                      ResourceType queryLevel,
+                      size_t limit);
+  };
+}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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<std::string>::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<std::string>::const_iterator
-           it = allowedValues_.begin(); it != allowedValues_.end(); ++it)
-    {
-      if (!s.empty())
-      {
-        s += "\\";
-      }
-
-      s += *it;
-    }
-
-    return s;
-  }
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IFindConstraint.h"
-
-#include <set>
-
-namespace Orthanc
-{
-  class ListConstraint : public IFindConstraint
-  {
-  private:
-    std::set<std::string>  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;
-  };
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "LookupIdentifierQuery.h"
-
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/OrthancException.h"
-#include "../ServerToolbox.h"
-#include "SetOfResources.h"
-
-#include <cassert>
-
-
-
-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<std::string>& 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<int64_t> a;
-
-      for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++)
-      {
-        const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j);
-        std::list<int64_t> 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<int64_t> 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;
-      }
-    }
-  }
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../IDatabaseWrapper.h"
-
-#include "SetOfResources.h"
-
-#include <vector>
-#include <boost/noncopyable.hpp>
-
-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<SingleConstraint*>  singleConstraints_;
-      std::vector<RangeConstraint*>   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<Disjunction*>  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<std::string>& result,
-               IDatabaseWrapper& database);
-
-    void Apply(SetOfResources& result,
-               IDatabaseWrapper& database);
-
-    void Print(std::ostream& s) const;
-  };
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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<IFindConstraint>& 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<IFindConstraint>& 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<IFindConstraint> 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<int64_t>  source;
-      candidates.Flatten(source);
-      printf("=> %d\n", source.size());
-      }*/
-
-    // Secondly, filter using the main DICOM tags
-    if (!identifiersConstraints_.empty() ||
-        !mainTagsConstraints_.empty())
-    {
-      std::list<int64_t>  source;
-      candidates.Flatten(source);
-      candidates.Clear();
-
-      std::list<int64_t>  filtered;
-      for (std::list<int64_t>::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<int64_t> allStudies, matchingStudies;
-      candidates.Flatten(allStudies);
- 
-      for (std::list<int64_t>::const_iterator
-             study = allStudies.begin(); study != allStudies.end(); ++study)
-      {
-        std::list<int64_t> childrenSeries;
-        database.GetChildrenInternalId(childrenSeries, *study);
-
-        for (std::list<int64_t>::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<int64_t>& 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<std::string> 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));
-    }
-  }
-
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "ListConstraint.h"
-#include "SetOfResources.h"
-
-#include <memory>
-
-namespace Orthanc
-{
-  class LookupResource : public boost::noncopyable
-  {
-  private:
-    typedef std::map<DicomTag, IFindConstraint*>  Constraints;
-    
-    class Level
-    {
-    private:
-      ResourceType        level_;
-      std::set<DicomTag>  identifiers_;
-      std::set<DicomTag>  mainTags_;
-      Constraints         identifiersConstraints_;
-      Constraints         mainTagsConstraints_;
-
-    public:
-      Level(ResourceType level);
-
-      ~Level();
-
-      bool Add(const DicomTag& tag,
-               std::auto_ptr<IFindConstraint>& constraint);
-
-      void Apply(SetOfResources& candidates,
-                 IDatabaseWrapper& database) const;
-
-      bool IsMatch(const DicomMap& dicom) const;
-    };
-
-    typedef std::map<ResourceType, Level*>  Levels;
-
-    ResourceType                    level_;
-    Levels                          levels_;
-    Constraints                     unoptimizedConstraints_;   // Constraints on non-main DICOM tags
-    std::auto_ptr<ListConstraint>   modalitiesInStudy_;
-
-    bool AddInternal(ResourceType level,
-                     const DicomTag& tag,
-                     std::auto_ptr<IFindConstraint>& 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<int64_t>& result,
-                        IDatabaseWrapper& database) const;
-
-    bool HasOnlyMainDicomTags() const
-    {
-      return unoptimizedConstraints_.empty();
-    }
-
-    bool IsMatch(const DicomMap& dicom) const;
-  };
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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_);
-  }
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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_;
-    }
-  };
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "SetOfResources.h"
-
-#include "../../Core/OrthancException.h"
-
-
-namespace Orthanc
-{
-  void SetOfResources::Intersect(const std::list<int64_t>& resources)
-  {
-    if (resources_.get() == NULL)
-    {
-      resources_.reset(new Resources);
-
-      for (std::list<int64_t>::const_iterator
-             it = resources.begin(); it != resources.end(); ++it)
-      {
-        resources_->insert(*it);
-      }
-    }
-    else
-    {
-      std::auto_ptr<Resources> filtered(new Resources);
-
-      for (std::list<int64_t>::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<Resources> children(new Resources);
-
-      for (Resources::const_iterator it = resources_->begin(); 
-           it != resources_->end(); ++it)
-      {
-        std::list<int64_t> tmp;
-        database_.GetChildrenInternalId(tmp, *it);
-
-        for (std::list<int64_t>::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<std::string>& 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<int64_t>& 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);
-      }
-    }
-  }
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../IDatabaseWrapper.h"
-
-#include <set>
-#include <boost/noncopyable.hpp>
-#include <memory>
-
-namespace Orthanc
-{
-  class SetOfResources : public boost::noncopyable
-  {
-  private:
-    typedef std::set<int64_t>  Resources;
-
-    IDatabaseWrapper&         database_;
-    ResourceType              level_;
-    std::auto_ptr<Resources>  resources_;
-    
-  public:
-    SetOfResources(IDatabaseWrapper& database,
-                   ResourceType level) : 
-      database_(database),
-      level_(level)
-    {
-    }
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    void Intersect(const std::list<int64_t>& resources);
-
-    void GoDown();
-
-    void Flatten(std::list<int64_t>& result);
-
-    void Flatten(std::list<std::string>& result);
-
-    void Clear()
-    {
-      resources_.reset(NULL);
-    }
-  };
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "ValueConstraint.h"
-
-#include "../../Core/Toolbox.h"
-
-#include <stdio.h>
-
-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);
-    }
-  }
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#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_;
-    }
-  };
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "WildcardConstraint.h"
-
-#include <boost/regex.hpp>
-
-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_;
-  }
-}
--- 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 <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "IFindConstraint.h"
-
-#include <boost/shared_ptr.hpp>
-
-namespace Orthanc
-{
-  class WildcardConstraint : public IFindConstraint
-  {
-  private:
-    struct PImpl;
-    boost::shared_ptr<PImpl>  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;
-  };
-}
--- 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<std::string> resources, instances;
 
-    std::vector<std::string> 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();
     
--- 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 <boost/filesystem.hpp>
-#include <boost/thread.hpp>
 
 
 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);
 
--- 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
 
--- 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 <boost/lexical_cast.hpp>
 #include <stdio.h>
@@ -59,6 +61,22 @@
 
 namespace Orthanc
 {
+  static void CopyListToVector(std::vector<std::string>& target,
+                               const std::list<std::string>& source)
+  {
+    target.resize(source.size());
+
+    size_t pos = 0;
+    
+    for (std::list<std::string>::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<SQLite::ITransaction> transaction_;
+    std::auto_ptr<IDatabaseWrapper::ITransaction> 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<int64_t>(sizeOfAddedFiles) -
+                         static_cast<int64_t>(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<DicomTag, TagInfo>   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<int64_t>(value->GetContent());
         int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent());
-        std::string expected = boost::lexical_cast<std::string>(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<int64_t>(value->GetContent());
         int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent());
-        std::string expected = boost::lexical_cast<std::string>(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<int64_t>(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<MetadataType, std::string>& instanceMetadata,
-                                        int64_t instance,
-                                        MetadataType metadata,
-                                        const std::string& value)
+  static void SetInstanceMetadata(ResourcesContent& content,
+                                  std::map<MetadataType, std::string>& 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<MetadataType, std::string>& 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<std::string>(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<std::string>(cs);
     target["TotalUncompressedSize"] = boost::lexical_cast<std::string>(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<int64_t> children;
-    db_.GetChildrenInternalId(children, id);
+    std::list<std::string> values;
+    db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries);
 
     std::set<int64_t> instances;
-    for (std::list<int64_t>::const_iterator 
-           it = children.begin(); it != children.end(); ++it)
+
+    for (std::list<std::string>::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<int64_t>(*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<int64_t>(instances.size()) == expected)
+    if (static_cast<int64_t>(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<int>(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<int>(i);
+        }
         else
+        {
           result["IndexInSeries"] = Json::nullValue;
+        }
 
         break;
       }
@@ -1187,7 +1308,9 @@
                         const std::list<T>& 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<T>::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<int>(last);
   }
 
@@ -1213,6 +1348,8 @@
   {
     std::list<ServerIndexChange> 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<ServerIndexChange> 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<std::string>& result,
+  void ServerIndex::LookupIdentifierExact(std::vector<std::string>& 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<DatabaseConstraint> query;
+    query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+
+    std::list<std::string> 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<std::string>& resources,
-                                   std::vector<std::string>& instances,
-                                   const ::Orthanc::LookupResource& lookup)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-   
-    std::list<int64_t> tmp;
-    lookup.FindCandidates(tmp, db_);
-
-    resources.resize(tmp.size());
-    instances.resize(tmp.size());
-
-    size_t pos = 0;
-    for (std::list<int64_t>::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<DatabaseConstraint>& 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<std::string>& resourcesId,
+                                         std::vector<std::string>* instancesId,
+                                         const DatabaseLookup& lookup,
+                                         ResourceType queryLevel,
+                                         size_t limit)
+  {
+    std::vector<DatabaseConstraint> normalized;
+    NormalizeLookup(normalized, lookup, queryLevel);
+
+    std::list<std::string> 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);
+    }
+  }
 }
--- 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 <boost/thread.hpp>
 #include <boost/noncopyable.hpp>
-#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<int64_t, UnstableResourcePayload>  unstableResources_;
 
-    uint64_t     currentStorageSize_;
     uint64_t     maximumStorageSize_;
     unsigned int maximumPatients_;
     bool         overwrite_;
+    std::auto_ptr<MainDicomTagsRegistry>  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<DatabaseConstraint>& target,
+                         const DatabaseLookup& source,
+                         ResourceType level) const;
 
-    void SetInstanceMetadata(std::map<MetadataType, std::string>& 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<std::string>& result,
+    void LookupIdentifierExact(std::vector<std::string>& result,
                                ResourceType level,
                                const DicomTag& tag,
                                const std::string& value);
@@ -284,14 +288,16 @@
 
     unsigned int GetDatabaseVersion();
 
-    void FindCandidates(std::vector<std::string>& resources,
-                        std::vector<std::string>& instances,
-                        const ::Orthanc::LookupResource& lookup);
-
     bool LookupParent(std::string& target,
                       const std::string& publicId,
                       ResourceType parentType);
 
     void ReconstructInstance(ParsedDicomFile& dicom);
+
+    void ApplyLookupResources(std::vector<std::string>& resourcesId,
+                              std::vector<std::string>* instancesId,  // Can be NULL if not needed
+                              const DatabaseLookup& lookup,
+                              ResourceType queryLevel,
+                              size_t limit);
   };
 }
--- 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 <stdio.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 <boost/shared_ptr.hpp>
+#include <stdint.h>
 
 namespace Orthanc
 {
+  class ServerContext;
+  
   class ArchiveJob : public IJob
   {
   private:
--- 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
 {
--- 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:
--- 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
 {
--- 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:
--- 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
--- 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:
--- 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"
--- 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:
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../../PrecompiledHeadersServer.h"
+#include "DicomInstanceOperationValue.h"
+
+#include "../../ServerContext.h"
+
+namespace Orthanc
+{
+  void DicomInstanceOperationValue::ReadDicom(std::string& dicom) const
+  {
+    context_.ReadDicom(dicom, id_);
+  }
+}
--- 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
     {
--- 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"
--- 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:
--- 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
 {
--- 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)
--- 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:
--- 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
--- 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:
--- 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
 {
--- 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:
--- 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
 {
--- 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:
--- 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 <cassert>
 
 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:
--- 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 <json/json.h>
+#include <boost/noncopyable.hpp>
+#include <list>
 
 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,
--- 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 <algorithm>
 #include <boost/lexical_cast.hpp>
--- 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:
--- 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;
--- 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;
--- 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<std::string>(currentVersion) + " to " +
                            boost::lexical_cast<std::string>(ORTHANC_DATABASE_VERSION) +
                            ": Please run Orthanc with the \"--upgrade\" argument");
--- 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 <cassert>
 
 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<ErrorCode>(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<int64_t>(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<int64_t>& 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<int64_t>& 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<int64_t> 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<ErrorCode>(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<const OrthancPluginChange*>(answer.valueGeneric);
+          const OrthancPluginChange& change =
+            *reinterpret_cast<const OrthancPluginChange*>(answer.valueGeneric);
           assert(answerChanges_ != NULL);
           answerChanges_->push_back
             (ServerIndexChange(change.seq,
@@ -1098,10 +1149,286 @@
         break;
       }
 
+      case _OrthancPluginDatabaseAnswerType_MatchingResource:
+      {
+        const OrthancPluginMatchingResource& match = 
+          *reinterpret_cast<const OrthancPluginMatchingResource*>(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<std::string>(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<std::string>& resourcesId,
+                                                   std::list<std::string>* instancesId,
+                                                   const std::vector<DatabaseConstraint>& 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<OrthancPluginDatabaseConstraint> constraints;
+      std::vector< std::vector<const char*> > 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<int64_t>& 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<int64_t>& 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<int64_t> 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<OrthancPluginResourcesContentTags> identifierTags;
+      std::vector<OrthancPluginResourcesContentTags> mainDicomTags;
+      std::vector<OrthancPluginResourcesContentMetadata> 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<std::string>& 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<int32_t>(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));
+    }
+  }
 }
--- 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<std::string>         answerStrings_;
     std::list<int32_t>             answerInt32_;
     std::list<int64_t>             answerInt64_;
@@ -67,6 +78,8 @@
     std::list<ServerIndexChange>*  answerChanges_;
     std::list<ExportedResource>*   answerExportedResources_;
     bool*                          answerDone_;
+    std::list<std::string>*        answerMatchingResources_;
+    std::list<std::string>*        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<MetadataType, std::string>& target,
-                                int64_t id);
-
-    virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                   ResourceType resourceType);
+                                int64_t id) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType);
+                                 ResourceType resourceType) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetAllPublicIds(std::list<std::string>& target,
                                  ResourceType resourceType,
                                  size_t since,
-                                 size_t limit);
+                                 size_t limit) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
                             bool& done /*out*/,
                             int64_t since,
-                            uint32_t maxResults);
+                            uint32_t maxResults) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id);
+                                       int64_t id) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id);
+                                     int64_t id) 
+      ORTHANC_OVERRIDE;
 
     virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
                                       bool& done /*out*/,
                                       int64_t since,
-                                      uint32_t maxResults);
+                                      uint32_t maxResults) 
+      ORTHANC_OVERRIDE;
 
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/);
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) 
+      ORTHANC_OVERRIDE;
 
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/);
+    virtual void GetLastExportedResource(std::list<ExportedResource>& 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<MetadataType>& target,
-                                       int64_t id);
+                                       int64_t id) 
+      ORTHANC_OVERRIDE;
 
     virtual void ListAvailableAttachments(std::list<FileContentType>& 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<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId,
+                                      const std::vector<DatabaseConstraint>& 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<int64_t>& target,
+                                   ResourceType resourceType) 
+      ORTHANC_OVERRIDE;
+
+    // From the "ILookupResources" interface
     virtual void LookupIdentifier(std::list<int64_t>& 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<int64_t>& 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<std::string>& 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;
   };
 }
 
--- 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<std::string> result;
+    std::vector<std::string> 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->()
--- 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)
--- 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);
--- 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, &params);
   }
 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerMatchingResource(
+    OrthancPluginContext*                 context,
+    OrthancPluginDatabaseContext*         database,
+    const OrthancPluginMatchingResource*  match)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_MatchingResource;
+    params.valueGeneric = match;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
   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;
 
 /*<! @endcond */
 
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Jan 24 10:54:47 2019 +0100
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Jan 24 10:55:19 2019 +0100
@@ -119,7 +119,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     5
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  0
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  2
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -821,6 +821,7 @@
   /**
    * The constraints on the DICOM identifiers that must be supported
    * by the database plugins.
+   * @deprecated Plugins using OrthancPluginConstraintType will be faster
    **/
   typedef enum
   {
@@ -834,6 +835,22 @@
 
 
   /**
+   * The constraints on the tags (main DICOM tags and identifier tags)
+   * that must be supported by the database plugins.
+   **/
+  typedef enum
+  {
+    OrthancPluginConstraintType_Equal = 1,           /*!< Equal */
+    OrthancPluginConstraintType_SmallerOrEqual = 2,  /*!< Less or equal */
+    OrthancPluginConstraintType_GreaterOrEqual = 3,  /*!< More or equal */
+    OrthancPluginConstraintType_Wildcard = 4,        /*!< Wildcard matching */
+    OrthancPluginConstraintType_List = 5,            /*!< List of values */
+
+    _OrthancPluginConstraintType_INTERNAL = 0x7fffffff
+  } OrthancPluginConstraintType;
+
+
+  /**
    * The origin of a DICOM instance that has been received by Orthanc.
    **/
   typedef enum
@@ -1513,7 +1530,8 @@
         sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) ||
         sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
         sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
-        sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus))
+        sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginConstraintType))
     {
       /* Mismatch in the size of the enumerations */
       return 0;
--- a/Resources/CMake/BoostConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/BoostConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -57,7 +57,7 @@
   set(BOOST_VERSION 1.68.0)
   set(BOOST_BCP_SUFFIX bcpdigest-1.5.0)
   set(BOOST_MD5 "5297c45ffda809b2da84223bac591abe")
-  set(BOOST_URL "http://www.orthanc-server.com/downloads/third-party/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz")
+  set(BOOST_URL "http://orthanc.osimis.io/ThirdPartyDownloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz")
   set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME})
 
   if (IS_DIRECTORY "${BOOST_SOURCES_DIR}")
--- a/Resources/CMake/CivetwebConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/CivetwebConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -1,10 +1,27 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_CIVETWEB)
   set(CIVETWEB_SOURCES_DIR ${CMAKE_BINARY_DIR}/civetweb-1.11)
-  set(CIVETWEB_URL "http://www.orthanc-server.com/downloads/third-party/civetweb-1.11.tar.gz")
+  set(CIVETWEB_URL "http://orthanc.osimis.io/ThirdPartyDownloads/civetweb-1.11.tar.gz")
   set(CIVETWEB_MD5 "b6d2175650a27924bccb747cbe084cd4")
 
+  if (IS_DIRECTORY "${CIVETWEB_SOURCES_DIR}")
+    set(FirstRun OFF)
+  else()
+    set(FirstRun ON)
+  endif()
+
   DownloadPackage(${CIVETWEB_MD5} ${CIVETWEB_URL} "${CIVETWEB_SOURCES_DIR}")
 
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${ORTHANC_ROOT}/Resources/Patches/civetweb-1.11.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (FirstRun AND Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+  
   include_directories(
     ${CIVETWEB_SOURCES_DIR}/include
     )
@@ -13,7 +30,6 @@
     ${CIVETWEB_SOURCES_DIR}/src/civetweb.c
     )
 
-
   if (ENABLE_SSL)
     add_definitions(
       -DNO_SSL_DL=1
@@ -29,13 +45,6 @@
       )
   endif()
 
-
-  if (CMAKE_SYSTEM_NAME STREQUAL "Windows" AND
-      CMAKE_COMPILER_IS_GNUCXX)
-    # This is a patch for MinGW64
-    add_definitions(-D_TIMESPEC_DEFINED=1)
-  endif()
-
   source_group(ThirdParty\\Civetweb REGULAR_EXPRESSION ${CIVETWEB_SOURCES_DIR}/.*)
 
 else()
--- a/Resources/CMake/Compiler.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/Compiler.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -73,9 +73,12 @@
     ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
     ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
 
-  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND
+      NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
     # The "--no-undefined" linker flag makes the shared libraries
-    # (plugins ModalityWorklists and ServeFolders) fail to compile on OpenBSD
+    # (plugins ModalityWorklists and ServeFolders) fail to compile on
+    # OpenBSD, and make the PostgreSQL plugin complain about missing
+    # "environ" global variable in FreeBSD
     set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined")
     set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
   endif()
--- a/Resources/CMake/DcmtkConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/DcmtkConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -7,13 +7,13 @@
     SET(DCMTK_VERSION_NUMBER 360)
     SET(DCMTK_PACKAGE_VERSION "3.6.0")
     SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.0)
-    SET(DCMTK_URL "http://www.orthanc-server.com/downloads/third-party/dcmtk-3.6.0.zip")
+    SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.0.zip")
     SET(DCMTK_MD5 "219ad631b82031806147e4abbfba4fa4")
   else()
     SET(DCMTK_VERSION_NUMBER 362)
     SET(DCMTK_PACKAGE_VERSION "3.6.2")
     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")
 
     macro(DCMTK_UNSET)
--- a/Resources/CMake/GoogleTestConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/GoogleTestConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -29,7 +29,7 @@
 
 elseif (STATIC_BUILD OR NOT USE_SYSTEM_GOOGLE_TEST)
   set(GOOGLE_TEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/gtest-1.7.0)
-  set(GOOGLE_TEST_URL "http://www.orthanc-server.com/downloads/third-party/gtest-1.7.0.zip")
+  set(GOOGLE_TEST_URL "http://orthanc.osimis.io/ThirdPartyDownloads/gtest-1.7.0.zip")
   set(GOOGLE_TEST_MD5 "2d6ec8ccdf5c46b05ba54a9fd1d130d7")
 
   DownloadPackage(${GOOGLE_TEST_MD5} ${GOOGLE_TEST_URL} "${GOOGLE_TEST_SOURCES_DIR}")
--- a/Resources/CMake/JsonCppConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/JsonCppConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -3,12 +3,12 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_JSONCPP)
   if (USE_LEGACY_JSONCPP)
     set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-0.10.6)
-    set(JSONCPP_URL "http://www.orthanc-server.com/downloads/third-party/jsoncpp-0.10.6.tar.gz")
+    set(JSONCPP_URL "http://orthanc.osimis.io/ThirdPartyDownloads/jsoncpp-0.10.6.tar.gz")
     set(JSONCPP_MD5 "13d1991d79697df8cadbc25c93e37c83")
     add_definitions(-DORTHANC_LEGACY_JSONCPP=1)
   else()
     set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-1.8.4)
-    set(JSONCPP_URL "http://www.orthanc-server.com/downloads/third-party/jsoncpp-1.8.4.tar.gz")
+    set(JSONCPP_URL "http://orthanc.osimis.io/ThirdPartyDownloads/jsoncpp-1.8.4.tar.gz")
     set(JSONCPP_MD5 "fa47a3ab6b381869b6a5f20811198662")
     add_definitions(-DORTHANC_LEGACY_JSONCPP=0)
     set(JSONCPP_CXX11 ON)
--- a/Resources/CMake/LibCurlConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/LibCurlConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -1,6 +1,6 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_CURL)
   SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-7.57.0)
-  SET(CURL_URL "http://www.orthanc-server.com/downloads/third-party/curl-7.57.0.tar.gz")
+  SET(CURL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/curl-7.57.0.tar.gz")
   SET(CURL_MD5 "c7aab73aaf5e883ca1d7518f93649dc2")
 
   if (IS_DIRECTORY "${CURL_SOURCES_DIR}")
--- a/Resources/CMake/LibIconvConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/LibIconvConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -9,7 +9,7 @@
 
   if (STATIC_BUILD OR NOT USE_SYSTEM_LIBICONV)
     set(LIBICONV_SOURCES_DIR ${CMAKE_BINARY_DIR}/libiconv-1.15)
-    set(LIBICONV_URL "http://www.orthanc-server.com/downloads/third-party/libiconv-1.15.tar.gz")
+    set(LIBICONV_URL "http://orthanc.osimis.io/ThirdPartyDownloads/libiconv-1.15.tar.gz")
     set(LIBICONV_MD5 "ace8b5f2db42f7b3b3057585e80d9808")
 
     DownloadPackage(${LIBICONV_MD5} ${LIBICONV_URL} "${LIBICONV_SOURCES_DIR}")
--- a/Resources/CMake/LibJpegConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/LibJpegConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -2,7 +2,7 @@
   set(LIBJPEG_SOURCES_DIR ${CMAKE_BINARY_DIR}/jpeg-9a)
   DownloadPackage(
     "3353992aecaee1805ef4109aadd433e7"
-    "http://www.orthanc-server.com/downloads/third-party/jpegsrc.v9a.tar.gz"
+    "http://orthanc.osimis.io/ThirdPartyDownloads/jpegsrc.v9a.tar.gz"
     "${LIBJPEG_SOURCES_DIR}")
 
   include_directories(
--- a/Resources/CMake/LibP11Configuration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/LibP11Configuration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -1,6 +1,6 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_LIBP11)
   SET(LIBP11_SOURCES_DIR ${CMAKE_BINARY_DIR}/libp11-0.4.0)
-  SET(LIBP11_URL "http://www.orthanc-server.com/downloads/third-party/beid/libp11-0.4.0.tar.gz")
+  SET(LIBP11_URL "http://orthanc.osimis.io/ThirdPartyDownloads/libp11-0.4.0.tar.gz")
   SET(LIBP11_MD5 "00b3e41db5be840d822bda12f3ab2ca7")
  
   if (IS_DIRECTORY "${LIBP11_SOURCES_DIR}")
--- a/Resources/CMake/LibPngConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/LibPngConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -1,6 +1,6 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_LIBPNG)
   SET(LIBPNG_SOURCES_DIR ${CMAKE_BINARY_DIR}/libpng-1.5.12)
-  SET(LIBPNG_URL "http://www.orthanc-server.com/downloads/third-party/libpng-1.5.12.tar.gz")
+  SET(LIBPNG_URL "http://orthanc.osimis.io/ThirdPartyDownloads/libpng-1.5.12.tar.gz")
   SET(LIBPNG_MD5 "8ea7f60347a306c5faf70b977fa80e28")
 
   DownloadPackage(${LIBPNG_MD5} ${LIBPNG_URL} "${LIBPNG_SOURCES_DIR}")
--- a/Resources/CMake/LuaConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/LuaConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -1,7 +1,7 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_LUA)
   SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.3.5)
   SET(LUA_MD5 "4f4b4f323fd3514a68e0ab3da8ce3455")
-  SET(LUA_URL "http://www.orthanc-server.com/downloads/third-party/lua-5.3.5.tar.gz")
+  SET(LUA_URL "http://orthanc.osimis.io/ThirdPartyDownloads/lua-5.3.5.tar.gz")
 
   DownloadPackage(${LUA_MD5} ${LUA_URL} "${LUA_SOURCES_DIR}")
 
--- a/Resources/CMake/MongooseConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/MongooseConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -11,7 +11,7 @@
     # Use Mongoose 3.1
     DownloadPackage(
       "e718fc287b4eb1bd523be3fa00942bb0"
-      "http://www.orthanc-server.com/downloads/third-party/mongoose-3.1.tgz"
+      "http://orthanc.osimis.io/ThirdPartyDownloads/mongoose-3.1.tgz"
       "${MONGOOSE_SOURCES_DIR}")
     
     add_definitions(-DMONGOOSE_USE_CALLBACKS=0)
@@ -21,7 +21,7 @@
     # Use Mongoose 3.8
     DownloadPackage(
       "7e3296295072792cdc3c633f9404e0c3"
-      "http://www.orthanc-server.com/downloads/third-party/mongoose-3.8.tgz"
+      "http://orthanc.osimis.io/ThirdPartyDownloads/mongoose-3.8.tgz"
       "${MONGOOSE_SOURCES_DIR}")
     
     add_definitions(-DMONGOOSE_USE_CALLBACKS=1)
--- a/Resources/CMake/OpenSslConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/OpenSslConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -1,6 +1,6 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_OPENSSL)
   SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.0.2o)
-  SET(OPENSSL_URL "http://www.orthanc-server.com/downloads/third-party/openssl-1.0.2o.tar.gz")
+  SET(OPENSSL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/openssl-1.0.2o.tar.gz")
   SET(OPENSSL_MD5 "44279b8557c3247cbe324e2322ecd114")
 
   if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}")
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -271,9 +271,9 @@
     ${ORTHANC_ROOT}/Core/HttpServer/HttpContentNegociation.cpp
     ${ORTHANC_ROOT}/Core/HttpServer/HttpFileSender.cpp
     ${ORTHANC_ROOT}/Core/HttpServer/HttpOutput.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/HttpServer.cpp
     ${ORTHANC_ROOT}/Core/HttpServer/HttpStreamTranscoder.cpp
     ${ORTHANC_ROOT}/Core/HttpServer/HttpToolbox.cpp
-    ${ORTHANC_ROOT}/Core/HttpServer/MongooseServer.cpp
     ${ORTHANC_ROOT}/Core/HttpServer/StringHttpOutput.cpp
     ${ORTHANC_ROOT}/Core/RestApi/RestApi.cpp
     ${ORTHANC_ROOT}/Core/RestApi/RestApiCall.cpp
--- a/Resources/CMake/OrthancFrameworkParameters.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/OrthancFrameworkParameters.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -17,7 +17,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "1.2")
+set(ORTHANC_API_VERSION "1.3")
 
 
 #####################################################################
@@ -30,7 +30,7 @@
 set(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)")
 
 # Generic parameters of the build
-set(ENABLE_CIVETWEB OFF CACHE BOOL "Use Civetweb instead of Mongoose (experimental)")
+set(ENABLE_CIVETWEB ON CACHE BOOL "Use Civetweb instead of Mongoose (Mongoose was the default embedded HTTP server in Orthanc <= 1.5.1)")
 set(ENABLE_PKCS11 OFF CACHE BOOL "Enable PKCS#11 for HTTPS client authentication using hardware security modules and smart cards")
 set(ENABLE_PROFILING OFF CACHE BOOL "Whether to enable the generation of profiling information with gprof")
 set(ENABLE_SSL ON CACHE BOOL "Include support for SSL")
--- a/Resources/CMake/PugixmlConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/PugixmlConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -1,7 +1,7 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML)
   set(PUGIXML_SOURCES_DIR ${CMAKE_BINARY_DIR}/pugixml-1.4)
   set(PUGIXML_MD5 "7c56c91cfe3ecdee248a8e4892ef5781")
-  set(PUGIXML_URL "http://www.orthanc-server.com/downloads/third-party/pugixml-1.4.tar.gz")
+  set(PUGIXML_URL "http://orthanc.osimis.io/ThirdPartyDownloads/pugixml-1.4.tar.gz")
 
   DownloadPackage(${PUGIXML_MD5} ${PUGIXML_URL} "${PUGIXML_SOURCES_DIR}")
 
--- a/Resources/CMake/SQLiteConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/SQLiteConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -17,7 +17,7 @@
 if (SQLITE_STATIC)
   SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3210000)
   SET(SQLITE_MD5 "fe330e88d81e77e1e61554a370ae5001")
-  SET(SQLITE_URL "http://www.orthanc-server.com/downloads/third-party/sqlite-amalgamation-3210000.zip")
+  SET(SQLITE_URL "http://orthanc.osimis.io/ThirdPartyDownloads/sqlite-amalgamation-3210000.zip")
 
   add_definitions(-DORTHANC_SQLITE_VERSION=3021000)
 
--- a/Resources/CMake/UuidConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/UuidConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -2,7 +2,7 @@
 
   if (STATIC_BUILD OR NOT USE_SYSTEM_UUID)
     SET(E2FSPROGS_SOURCES_DIR ${CMAKE_BINARY_DIR}/e2fsprogs-1.43.8)
-    SET(E2FSPROGS_URL "http://www.orthanc-server.com/downloads/third-party/e2fsprogs-1.43.8.tar.gz")
+    SET(E2FSPROGS_URL "http://orthanc.osimis.io/ThirdPartyDownloads/e2fsprogs-1.43.8.tar.gz")
     SET(E2FSPROGS_MD5 "670b7a74a8ead5333acf21b9afc92b3c")
 
     if (IS_DIRECTORY "${E2FSPROGS_SOURCES_DIR}")
--- a/Resources/CMake/ZlibConfiguration.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/CMake/ZlibConfiguration.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -1,6 +1,6 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_ZLIB)
   SET(ZLIB_SOURCES_DIR ${CMAKE_BINARY_DIR}/zlib-1.2.11)
-  SET(ZLIB_URL "http://www.orthanc-server.com/downloads/third-party/zlib-1.2.11.tar.gz")
+  SET(ZLIB_URL "http://orthanc.osimis.io/ThirdPartyDownloads/zlib-1.2.11.tar.gz")
   SET(ZLIB_MD5 "1c9f62f0778697a09d36121ead88e08e")
 
   DownloadPackage(${ZLIB_MD5} ${ZLIB_URL} "${ZLIB_SOURCES_DIR}")
--- a/Resources/Configuration.json	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/Configuration.json	Thu Jan 24 10:55:19 2019 +0100
@@ -346,9 +346,22 @@
   // default behavior since Orthanc 1.4.0).
   "LogExportedResources" : false,
 
-  // Enable or disable HTTP Keep-Alive (deprecated). Set this option
-  // to "true" only in the case of high HTTP loads.
-  "KeepAlive" : false,
+  // Enable or disable HTTP Keep-Alive (persistent HTTP
+  // connections). Setting this option to "true" prevents Orthanc
+  // issue #32 ("HttpServer does not support multiple HTTP requests in
+  // the same TCP stream"), but can possibly slow down HTTP clients
+  // that do not support persistent connections. The default behavior
+  // used to be "false" in Orthanc <= 1.5.1. Setting this option to
+  // "false" is also recommended if Orthanc is compiled against
+  // Mongoose.
+  "KeepAlive" : true,
+
+  // Enable or disable Nagle's algorithm. Only taken into
+  // consideration if Orthanc is compiled to use CivetWeb. Experiments
+  // show that best performance can be obtained by setting both
+  // "KeepAlive" and "TcpNoDelay" to "true". Beware however of
+  // caveats: https://eklitzke.org/the-caveats-of-tcp-nodelay
+  "TcpNoDelay" : true,
 
   // If this option is set to "false", Orthanc will run in index-only
   // mode. The DICOM files will not be stored on the drive. Note that
--- a/Resources/DownloadOrthancFramework.cmake	Thu Jan 24 10:54:47 2019 +0100
+++ b/Resources/DownloadOrthancFramework.cmake	Thu Jan 24 10:55:19 2019 +0100
@@ -95,6 +95,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "4429d8d9dea4ff6648df80ec3c64d79e")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.1")
         set(ORTHANC_FRAMEWORK_MD5 "099671538865e5da96208b37494d6718")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.2")
+        set(ORTHANC_FRAMEWORK_MD5 "8867050f3e9a1ce6157c1ea7a9433b1b")
       endif()
     endif()
   endif()
@@ -229,8 +231,7 @@
   else()
     # Default case: Download from the official Web site
     set(ORTHANC_FRAMEMORK_FILENAME Orthanc-${ORTHANC_FRAMEWORK_VERSION}.tar.gz)
-    #set(ORTHANC_FRAMEWORK_URL "http://www.orthanc-server.com/downloads/get.php?path=/orthanc/${ORTHANC_FRAMEMORK_FILENAME}")
-    set(ORTHANC_FRAMEWORK_URL "http://www.orthanc-server.com/downloads/third-party/orthanc-framework/${ORTHANC_FRAMEMORK_FILENAME}")
+    set(ORTHANC_FRAMEWORK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/orthanc-framework/${ORTHANC_FRAMEMORK_FILENAME}")
   endif()
 
   set(ORTHANC_FRAMEWORK_ARCHIVE "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${ORTHANC_FRAMEMORK_FILENAME}")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp	Thu Jan 24 10:55:19 2019 +0100
@@ -0,0 +1,215 @@
+/**
+ * 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://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "LookupIdentifierQuery.h"
+
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/OrthancException.h"
+#include "../ServerToolbox.h"
+#include "SetOfResources.h"
+
+#include <cassert>
+
+
+
+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<std::string>& 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<int64_t> a;
+
+      for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++)
+      {
+        const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j);
+        std::list<int64_t> 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<int64_t> 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;
+      }
+    }
+  }
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../IDatabaseWrapper.h"
+
+#include "SetOfResources.h"
+
+#include <vector>
+#include <boost/noncopyable.hpp>
+
+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<SingleConstraint*>  singleConstraints_;
+      std::vector<RangeConstraint*>   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<Disjunction*>  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<std::string>& result,
+               IDatabaseWrapper& database);
+
+    void Apply(SetOfResources& result,
+               IDatabaseWrapper& database);
+
+    void Print(std::ostream& s) const;
+  };
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#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<IFindConstraint>& 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<IFindConstraint>& 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<IFindConstraint> 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<int64_t>  source;
+      candidates.Flatten(source);
+      printf("=> %d\n", source.size());
+      }*/
+
+    // Secondly, filter using the main DICOM tags
+    if (!identifiersConstraints_.empty() ||
+        !mainTagsConstraints_.empty())
+    {
+      std::list<int64_t>  source;
+      candidates.Flatten(source);
+      candidates.Clear();
+
+      std::list<int64_t>  filtered;
+      for (std::list<int64_t>::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<int64_t> allStudies, matchingStudies;
+      candidates.Flatten(allStudies);
+ 
+      for (std::list<int64_t>::const_iterator
+             study = allStudies.begin(); study != allStudies.end(); ++study)
+      {
+        std::list<int64_t> childrenSeries;
+        database.GetChildrenInternalId(childrenSeries, *study);
+
+        for (std::list<int64_t>::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<int64_t>& 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<std::string> 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));
+    }
+  }
+
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ListConstraint.h"
+#include "SetOfResources.h"
+
+#include <memory>
+
+namespace Orthanc
+{
+  class LookupResource : public boost::noncopyable
+  {
+  private:
+    typedef std::map<DicomTag, IFindConstraint*>  Constraints;
+    
+    class Level
+    {
+    private:
+      ResourceType        level_;
+      std::set<DicomTag>  identifiers_;
+      std::set<DicomTag>  mainTags_;
+      Constraints         identifiersConstraints_;
+      Constraints         mainTagsConstraints_;
+
+    public:
+      Level(ResourceType level);
+
+      ~Level();
+
+      bool Add(const DicomTag& tag,
+               std::auto_ptr<IFindConstraint>& constraint);
+
+      void Apply(SetOfResources& candidates,
+                 IDatabaseWrapper& database) const;
+
+      bool IsMatch(const DicomMap& dicom) const;
+    };
+
+    typedef std::map<ResourceType, Level*>  Levels;
+
+    ResourceType                    level_;
+    Levels                          levels_;
+    Constraints                     unoptimizedConstraints_;   // Constraints on non-main DICOM tags
+    std::auto_ptr<ListConstraint>   modalitiesInStudy_;
+
+    bool AddInternal(ResourceType level,
+                     const DicomTag& tag,
+                     std::auto_ptr<IFindConstraint>& 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<int64_t>& result,
+                        IDatabaseWrapper& database) const;
+
+    bool HasOnlyMainDicomTags() const
+    {
+      return unoptimizedConstraints_.empty();
+    }
+
+    bool IsMatch(const DicomMap& dicom) const;
+  };
+}
--- /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 */
--- 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);
--- /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
--- /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 <Core/HttpClient.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/SystemToolbox.h>
+
+#include <iostream>
+#include <boost/thread.hpp>
+
+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<boost::thread*> 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;
+}
--- /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
--- /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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>io.osimis</groupId>
+  <artifactId>issue32</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>issue32</name>
+  <!-- FIXME change it to the project's website -->
+  <url>http://www.example.com</url>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <maven.compiler.source>1.8</maven.compiler.source>
+    <maven.compiler.target>1.8</maven.compiler.target>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+      <version>4.5.3</version>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.11</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
--- /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);
+  }
+}
--- 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}")
--- 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
--- 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<std::string>& 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<std::string>& 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());
   }
 }
--- 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 <dcmtk/dcmdata/dcelem.h>
 #include <dcmtk/dcmdata/dcdeftag.h>
--- 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<ServerContext>   context_;
     TimeoutDicomConnectionManager  manager_;
 
--- 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 <ctype.h>
@@ -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<DatabaseWrapperClass>
+  class DatabaseWrapperTest : public ::testing::Test
   {
   protected:
-    std::auto_ptr<TestDatabaseListener> listener_;
-    std::auto_ptr<IDatabaseWrapper> index_;
+    std::auto_ptr<TestDatabaseListener>  listener_;
+    std::auto_ptr<SQLiteDatabaseWrapper> 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<DatabaseWrapper*>(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<DatabaseWrapper*>(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<DatabaseWrapper*>(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<std::string> j;
-
-      switch (GetParam())
-      {
-        case DatabaseWrapperClass_SQLite:
-        {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(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<std::string> j;
-
-      switch (GetParam())
-      {
-        case DatabaseWrapperClass_SQLite:
-        {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(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<std::string> j;
-
-      switch (GetParam())
-      {
-        case DatabaseWrapperClass_SQLite:
-        {
-          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(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<std::string>& result,
-                  ResourceType level,
-                  const DicomTag& tag,
-                  const std::string& value)
+    void DoLookupIdentifier(std::list<std::string>& 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<DatabaseConstraint> lookup;
+      lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      
+      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
+    }    
+
+    void DoLookupIdentifier2(std::list<std::string>& 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<DatabaseConstraint> 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<int64_t> patients;
   for (int i = 0; i < 10; i++)
@@ -587,7 +524,7 @@
 }
 
 
-TEST_P(DatabaseWrapperTest, PatientProtection)
+TEST_F(DatabaseWrapperTest, PatientProtection)
 {
   std::vector<int64_t> 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<std::string> 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);
--- 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 <endian.h>
 #elif defined(__FreeBSD__)