diff -pruN 1.5.3+dfsg-1/CMakeLists.txt 1.5.4+dfsg-1/CMakeLists.txt
--- 1.5.3+dfsg-1/CMakeLists.txt	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/CMakeLists.txt	2019-02-08 14:02:50.000000000 +0000
@@ -56,6 +56,7 @@ set(ORTHANC_SERVER_SOURCES
   OrthancServer/Database/Compatibility/DatabaseLookup.cpp
   OrthancServer/Database/Compatibility/ICreateInstance.cpp
   OrthancServer/Database/Compatibility/IGetChildrenMetadata.cpp
+  OrthancServer/Database/Compatibility/ILookupResourceAndParent.cpp
   OrthancServer/Database/Compatibility/ILookupResources.cpp
   OrthancServer/Database/Compatibility/SetOfResources.cpp
   OrthancServer/Database/ResourcesContent.cpp
diff -pruN 1.5.3+dfsg-1/Core/Compression/ZipWriter.cpp 1.5.4+dfsg-1/Core/Compression/ZipWriter.cpp
--- 1.5.3+dfsg-1/Core/Compression/ZipWriter.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/Compression/ZipWriter.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -148,7 +148,8 @@ namespace Orthanc
 
     if (!pimpl_->file_)
     {
-      throw OrthancException(ErrorCode_CannotWriteFile);
+      throw OrthancException(ErrorCode_CannotWriteFile,
+                             "Cannot create new ZIP archive: " + path_);
     }
   }
 
@@ -169,7 +170,8 @@ namespace Orthanc
     if (level >= 10)
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "ZIP compression level must be between 0 (no compression) and 9 (highest compression)");
+                             "ZIP compression level must be between 0 (no compression) "
+                             "and 9 (highest compression)");
     }
 
     Close();
@@ -208,7 +210,8 @@ namespace Orthanc
 
     if (result != 0)
     {
-      throw OrthancException(ErrorCode_CannotWriteFile);
+      throw OrthancException(ErrorCode_CannotWriteFile,
+                             "Cannot add new file inside ZIP archive: " + std::string(path));
     }
 
     hasFileInZip_ = true;
@@ -239,7 +242,8 @@ namespace Orthanc
 
       if (zipWriteInFileInZip(pimpl_->file_, data, bytes))
       {
-        throw OrthancException(ErrorCode_CannotWriteFile);
+        throw OrthancException(ErrorCode_CannotWriteFile,
+                               "Cannot write data to ZIP archive: " + path_);
       }
       
       data += bytes;
@@ -253,6 +257,4 @@ namespace Orthanc
     Close();
     append_ = append;
   }
-    
-
 }
diff -pruN 1.5.3+dfsg-1/Core/DicomFormat/DicomTag.h 1.5.4+dfsg-1/Core/DicomFormat/DicomTag.h
--- 1.5.3+dfsg-1/Core/DicomFormat/DicomTag.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/DicomFormat/DicomTag.h	2019-02-08 14:02:50.000000000 +0000
@@ -174,6 +174,9 @@ namespace Orthanc
   static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION(0x0040, 0x0254);
   static const DicomTag DICOM_TAG_IMAGE_COMMENTS(0x0020, 0x4000);
   static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION(0x0018, 0x1400);
+  static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_CODE(0x0018, 0x1401);
+  static const DicomTag DICOM_TAG_CASSETTE_ORIENTATION(0x0018, 0x1402);
+  static const DicomTag DICOM_TAG_CASSETTE_SIZE(0x0018, 0x1403);
   static const DicomTag DICOM_TAG_CONTRAST_BOLUS_AGENT(0x0018, 0x0010);
   static const DicomTag DICOM_TAG_STUDY_ID(0x0020, 0x0010);
   static const DicomTag DICOM_TAG_SERIES_NUMBER(0x0020, 0x0011);
diff -pruN 1.5.3+dfsg-1/Core/DicomNetworking/DicomUserConnection.cpp 1.5.4+dfsg-1/Core/DicomNetworking/DicomUserConnection.cpp
--- 1.5.3+dfsg-1/Core/DicomNetworking/DicomUserConnection.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/DicomNetworking/DicomUserConnection.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -164,12 +164,50 @@ namespace Orthanc
   };
 
 
-  static void Check(const OFCondition& cond)
+  static void Check(const OFCondition& cond,
+                    const std::string& aet)
   {
     if (cond.bad())
     {
+      // Reformat the error message from DCMTK by turning multiline
+      // errors into a single line
+      
+      std::string s(cond.text());
+      std::string info;
+      info.reserve(s.size());
+
+      bool isMultiline = false;
+      for (size_t i = 0; i < s.size(); i++)
+      {
+        if (s[i] == '\r')
+        {
+          // Ignore
+        }
+        else if (s[i] == '\n')
+        {
+          if (isMultiline)
+          {
+            info += "; ";
+          }
+          else
+          {
+            info += " (";
+            isMultiline = true;
+          }
+        }
+        else
+        {
+          info.push_back(s[i]);
+        }
+      }
+
+      if (isMultiline)
+      {
+        info += ")";
+      }
+      
       throw OrthancException(ErrorCode_NetworkProtocol,
-                             "DicomUserConnection: " + std::string(cond.text()));
+                             "DicomUserConnection to AET \"" + aet + "\": " + info);
     }
   }
 
@@ -193,16 +231,17 @@ namespace Orthanc
                                       unsigned int& presentationContextId,
                                       const std::string& sopClass,
                                       const char* asPreferred[],
-                                      std::vector<const char*>& asFallback)
+                                      std::vector<const char*>& asFallback,
+                                      const std::string& aet)
   {
     Check(ASC_addPresentationContext(params, presentationContextId, 
-                                     sopClass.c_str(), asPreferred, 1));
+                                     sopClass.c_str(), asPreferred, 1), aet);
     presentationContextId += 2;
 
     if (asFallback.size() > 0)
     {
       Check(ASC_addPresentationContext(params, presentationContextId, 
-                                       sopClass.c_str(), &asFallback[0], asFallback.size()));
+                                       sopClass.c_str(), &asFallback[0], asFallback.size()), aet);
       presentationContextId += 2;
     }
   }
@@ -236,21 +275,21 @@ namespace Orthanc
          it != reservedStorageSOPClasses_.end(); ++it)
     {
       RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
-                              *it, asPreferred, asFallback);
+                              *it, asPreferred, asFallback, remoteAet_);
     }
 
     for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin();
          it != storageSOPClasses_.end(); ++it)
     {
       RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
-                              *it, asPreferred, asFallback);
+                              *it, asPreferred, asFallback, remoteAet_);
     }
 
     for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin();
          it != defaultStorageSOPClasses_.end(); ++it)
     {
       RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
-                              *it, asPreferred, asFallback);
+                              *it, asPreferred, asFallback, remoteAet_);
     }
   }
 
@@ -269,7 +308,7 @@ namespace Orthanc
                                          uint16_t moveOriginatorID)
   {
     DcmFileFormat dcmff;
-    Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength));
+    Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength), connection.remoteAet_);
 
     // Determine the storage SOP class UID for this instance
     static const DcmTagKey DCM_SOP_CLASS_UID(0x0008, 0x0016);
@@ -379,7 +418,7 @@ namespace Orthanc
     Check(DIMSE_storeUser(assoc_, presID, &request,
                           NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL,
                           /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_,
-                          &rsp, &statusDetail, NULL));
+                          &rsp, &statusDetail, NULL), connection.remoteAet_);
 
     if (statusDetail != NULL) 
     {
@@ -556,7 +595,8 @@ namespace Orthanc
                           const char* sopClass,
                           bool isWorklist,
                           const char* level,
-                          uint32_t dimseTimeout)
+                          uint32_t dimseTimeout,
+                          const std::string& remoteAet)
   {
     assert(isWorklist ^ (level != NULL));
 
@@ -600,7 +640,7 @@ namespace Orthanc
       delete statusDetail;
     }
 
-    Check(cond);
+    Check(cond, remoteAet);
   }
 
 
@@ -720,7 +760,8 @@ namespace Orthanc
     }
 
     assert(clevel != NULL && sopClass != NULL);
-    ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, false, clevel, pimpl_->dimseTimeout_);
+    ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, false, clevel,
+                pimpl_->dimseTimeout_, remoteAet_);
   }
 
 
@@ -803,7 +844,7 @@ namespace Orthanc
       delete responseIdentifiers;
     }
 
-    Check(cond);
+    Check(cond, remoteAet_);
   }
 
 
@@ -972,11 +1013,11 @@ namespace Orthanc
               << GetRemoteHost() << ":" << GetRemotePort() 
               << " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")";
 
-    Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_));
-    Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
+    Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_), remoteAet_);
+    Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU), remoteAet_);
 
     // Set this application's title and the called application's title in the params
-    Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL));
+    Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL), remoteAet_);
 
     // Set the network addresses of the local and remote entities
     char localHost[HOST_NAME_MAX];
@@ -991,15 +1032,15 @@ namespace Orthanc
 #endif
       (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", remoteHost_.c_str(), remotePort_);
 
-    Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort));
+    Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort), remoteAet_);
 
     // Set various options
-    Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false));
+    Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false), remoteAet_);
 
     SetupPresentationContexts(preferredTransferSyntax_);
 
     // Do the association
-    Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_));
+    Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_), remoteAet_);
 
     if (ASC_countAcceptedPresentationContexts(pimpl_->params_) == 0)
     {
@@ -1077,7 +1118,7 @@ namespace Orthanc
     Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, 
                          /*opt_blockMode*/ DIMSE_BLOCKING, 
                          /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
-                         &status, NULL));
+                         &status, NULL), remoteAet_);
     return status == STATUS_Success;
   }
 
@@ -1276,7 +1317,8 @@ namespace Orthanc
     DcmDataset* dataset = query.GetDcmtkObject().getDataset();
     const char* sopClass = UID_FINDModalityWorklistInformationModel;
 
-    ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true, NULL, pimpl_->dimseTimeout_);
+    ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true,
+                NULL, pimpl_->dimseTimeout_, remoteAet_);
   }
 
   
diff -pruN 1.5.3+dfsg-1/Core/DicomParsing/DicomModification.cpp 1.5.4+dfsg-1/Core/DicomParsing/DicomModification.cpp
--- 1.5.3+dfsg-1/Core/DicomParsing/DicomModification.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/DicomParsing/DicomModification.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -79,10 +79,16 @@ namespace Orthanc
     {
     }
 
-    virtual void VisitUnknown(const std::vector<DicomTag>& parentTags,
-                              const std::vector<size_t>& parentIndexes,
-                              const DicomTag& tag,
-                              ValueRepresentation vr)
+    virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   ValueRepresentation vr)
+    {
+    }
+
+    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    const DicomTag& tag)
     {
     }
 
@@ -95,27 +101,26 @@ namespace Orthanc
     {
     }
 
-    virtual void VisitInteger(const std::vector<DicomTag>& parentTags,
-                              const std::vector<size_t>& parentIndexes,
-                              const DicomTag& tag,
-                              ValueRepresentation vr,
-                              int64_t value)
+    virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::vector<int64_t>& values)
     {
     }
 
-    virtual void VisitDouble(const std::vector<DicomTag>& parentTags,
-                             const std::vector<size_t>& parentIndexes,
-                             const DicomTag& tag,
-                             ValueRepresentation vr,
-                             double value)
+    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
+                              const std::vector<size_t>& parentIndexes,
+                              const DicomTag& tag,
+                              ValueRepresentation vr,
+                              const std::vector<double>& value)
     {
     }
 
-    virtual void VisitAttribute(const std::vector<DicomTag>& parentTags,
-                                const std::vector<size_t>& parentIndexes,
-                                const DicomTag& tag,
-                                ValueRepresentation vr,
-                                const DicomTag& value)
+    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 const std::vector<DicomTag>& value)
     {
     }
 
diff -pruN 1.5.3+dfsg-1/Core/DicomParsing/DicomWebJsonVisitor.cpp 1.5.4+dfsg-1/Core/DicomParsing/DicomWebJsonVisitor.cpp
--- 1.5.3+dfsg-1/Core/DicomParsing/DicomWebJsonVisitor.cpp	1970-01-01 00:00:00.000000000 +0000
+++ 1.5.4+dfsg-1/Core/DicomParsing/DicomWebJsonVisitor.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -0,0 +1,575 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "DicomWebJsonVisitor.h"
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+#include "FromDcmtkBridge.h"
+
+#include <boost/math/special_functions/round.hpp>
+#include <boost/lexical_cast.hpp>
+
+
+static const char* const KEY_ALPHABETIC = "Alphabetic";
+static const char* const KEY_BULK_DATA_URI = "BulkDataURI";
+static const char* const KEY_INLINE_BINARY = "InlineBinary";
+static const char* const KEY_SQ = "SQ";
+static const char* const KEY_VALUE = "Value";
+static const char* const KEY_VR = "vr";
+
+
+namespace Orthanc
+{
+#if ORTHANC_ENABLE_PUGIXML == 1
+  static void ExploreXmlDataset(pugi::xml_node& target,
+                                const Json::Value& source)
+  {
+    assert(source.type() == Json::objectValue);
+
+    Json::Value::Members members = source.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const DicomTag tag = FromDcmtkBridge::ParseTag(members[i]);
+      const Json::Value& content = source[members[i]];
+
+      assert(content.type() == Json::objectValue &&
+             content.isMember("vr") &&
+             content["vr"].type() == Json::stringValue);
+      const std::string vr = content["vr"].asString();
+
+      const std::string keyword = FromDcmtkBridge::GetTagName(tag, "");
+    
+      pugi::xml_node node = target.append_child("DicomAttribute");
+      node.append_attribute("tag").set_value(members[i].c_str());
+      node.append_attribute("vr").set_value(vr.c_str());
+
+      if (keyword != std::string(DcmTag_ERROR_TagName))
+      {
+        node.append_attribute("keyword").set_value(keyword.c_str());
+      }   
+
+      if (content.isMember(KEY_VALUE))
+      {
+        assert(content[KEY_VALUE].type() == Json::arrayValue);
+        
+        for (Json::Value::ArrayIndex j = 0; j < content[KEY_VALUE].size(); j++)
+        {
+          std::string number = boost::lexical_cast<std::string>(j + 1);
+
+          if (vr == "SQ")
+          {
+            if (content[KEY_VALUE][j].type() == Json::objectValue)
+            {
+              pugi::xml_node child = node.append_child("Item");
+              child.append_attribute("number").set_value(number.c_str());
+              ExploreXmlDataset(child, content[KEY_VALUE][j]);
+            }
+          }
+          if (vr == "PN")
+          {
+            if (content[KEY_VALUE][j].isMember(KEY_ALPHABETIC) &&
+                content[KEY_VALUE][j][KEY_ALPHABETIC].type() == Json::stringValue)
+            {
+              std::vector<std::string> tokens;
+              Toolbox::TokenizeString(tokens, content[KEY_VALUE][j][KEY_ALPHABETIC].asString(), '^');
+
+              pugi::xml_node child = node.append_child("PersonName");
+              child.append_attribute("number").set_value(number.c_str());
+            
+              pugi::xml_node name = child.append_child(KEY_ALPHABETIC);
+            
+              if (tokens.size() >= 1)
+              {
+                name.append_child("FamilyName").text() = tokens[0].c_str();
+              }
+            
+              if (tokens.size() >= 2)
+              {
+                name.append_child("GivenName").text() = tokens[1].c_str();
+              }
+            
+              if (tokens.size() >= 3)
+              {
+                name.append_child("MiddleName").text() = tokens[2].c_str();
+              }
+            
+              if (tokens.size() >= 4)
+              {
+                name.append_child("NamePrefix").text() = tokens[3].c_str();
+              }
+            
+              if (tokens.size() >= 5)
+              {
+                name.append_child("NameSuffix").text() = tokens[4].c_str();
+              }
+            }
+          }
+          else
+          {
+            pugi::xml_node child = node.append_child("Value");
+            child.append_attribute("number").set_value(number.c_str());
+
+            switch (content[KEY_VALUE][j].type())
+            {
+              case Json::stringValue:
+                child.text() = content[KEY_VALUE][j].asCString();
+                break;
+
+              case Json::realValue:
+                child.text() = content[KEY_VALUE][j].asFloat();
+                break;
+
+              case Json::intValue:
+                child.text() = content[KEY_VALUE][j].asInt();
+                break;
+
+              case Json::uintValue:
+                child.text() = content[KEY_VALUE][j].asUInt();
+                break;
+
+              default:
+                break;
+            }
+          }
+        }
+      }
+      else if (content.isMember(KEY_BULK_DATA_URI) &&
+               content[KEY_BULK_DATA_URI].type() == Json::stringValue)
+      {
+        pugi::xml_node child = node.append_child("BulkData");
+        child.append_attribute("URI").set_value(content[KEY_BULK_DATA_URI].asCString());
+      }
+      else if (content.isMember(KEY_INLINE_BINARY) &&
+               content[KEY_INLINE_BINARY].type() == Json::stringValue)
+      {
+        pugi::xml_node child = node.append_child("InlineBinary");
+        child.text() = content[KEY_INLINE_BINARY].asCString();
+      }
+    }
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  static void DicomWebJsonToXml(pugi::xml_document& target,
+                                const Json::Value& source)
+  {
+    pugi::xml_node root = target.append_child("NativeDicomModel");
+    root.append_attribute("xmlns").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
+    root.append_attribute("xsi:schemaLocation").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
+    root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance");
+
+    ExploreXmlDataset(root, source);
+
+    pugi::xml_node decl = target.prepend_child(pugi::node_declaration);
+    decl.append_attribute("version").set_value("1.0");
+    decl.append_attribute("encoding").set_value("utf-8");
+  }
+#endif
+
+
+  std::string DicomWebJsonVisitor::FormatTag(const DicomTag& tag)
+  {
+    char buf[16];
+    sprintf(buf, "%04X%04X", tag.GetGroup(), tag.GetElement());
+    return std::string(buf);
+  }
+
+    
+  Json::Value& DicomWebJsonVisitor::CreateNode(const std::vector<DicomTag>& parentTags,
+                                               const std::vector<size_t>& parentIndexes,
+                                               const DicomTag& tag)
+  {
+    assert(parentTags.size() == parentIndexes.size());      
+
+    Json::Value* node = &result_;
+
+    for (size_t i = 0; i < parentTags.size(); i++)
+    {
+      std::string t = FormatTag(parentTags[i]);
+
+      if (!node->isMember(t))
+      {
+        Json::Value item = Json::objectValue;
+        item[KEY_VR] = KEY_SQ;
+        item[KEY_VALUE] = Json::arrayValue;
+        item[KEY_VALUE].append(Json::objectValue);
+        (*node) [t] = item;
+
+        node = &(*node)[t][KEY_VALUE][0];
+      }
+      else if ((*node)  [t].type() != Json::objectValue ||
+               !(*node) [t].isMember(KEY_VR) ||
+               (*node)  [t][KEY_VR].type() != Json::stringValue ||
+               (*node)  [t][KEY_VR].asString() != KEY_SQ ||
+               !(*node) [t].isMember(KEY_VALUE) ||
+               (*node)  [t][KEY_VALUE].type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        size_t currentSize = (*node) [t][KEY_VALUE].size();
+
+        if (parentIndexes[i] < currentSize)
+        {
+          // The node already exists
+        }
+        else if (parentIndexes[i] == currentSize)
+        {
+          (*node) [t][KEY_VALUE].append(Json::objectValue);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+          
+        node = &(*node) [t][KEY_VALUE][Json::ArrayIndex(parentIndexes[i])];
+      }
+    }
+
+    assert(node->type() == Json::objectValue);
+
+    std::string t = FormatTag(tag);
+    if (node->isMember(t))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      (*node) [t] = Json::objectValue;
+      return (*node) [t];
+    }
+  }
+
+    
+  Json::Value DicomWebJsonVisitor::FormatInteger(int64_t value)
+  {
+    if (value < 0)
+    {
+      return Json::Value(static_cast<int32_t>(value));
+    }
+    else
+    {
+      return Json::Value(static_cast<uint32_t>(value));
+    }
+  }
+
+    
+  Json::Value DicomWebJsonVisitor::FormatDouble(double value)
+  {
+    long long a = boost::math::llround<double>(value);
+
+    double d = fabs(value - static_cast<double>(a));
+
+    if (d <= std::numeric_limits<double>::epsilon() * 100.0)
+    {
+      return FormatInteger(a);
+    }
+    else
+    {
+      return Json::Value(value);
+    }
+  }
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  void DicomWebJsonVisitor::FormatXml(std::string& target) const
+  {
+    pugi::xml_document doc;
+    DicomWebJsonToXml(doc, result_);
+    Toolbox::XmlToString(target, doc);
+  }
+#endif
+
+
+  void DicomWebJsonVisitor::VisitEmptySequence(const std::vector<DicomTag>& parentTags,
+                                               const std::vector<size_t>& parentIndexes,
+                                               const DicomTag& tag)
+  {
+    if (tag.GetElement() != 0x0000)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(ValueRepresentation_Sequence);
+    }
+  }
+  
+
+  void DicomWebJsonVisitor::VisitBinary(const std::vector<DicomTag>& parentTags,
+                                        const std::vector<size_t>& parentIndexes,
+                                        const DicomTag& tag,
+                                        ValueRepresentation vr,
+                                        const void* data,
+                                        size_t size)
+  {
+    assert(vr == ValueRepresentation_OtherByte ||
+           vr == ValueRepresentation_OtherDouble ||
+           vr == ValueRepresentation_OtherFloat ||
+           vr == ValueRepresentation_OtherLong ||
+           vr == ValueRepresentation_OtherWord ||
+           vr == ValueRepresentation_Unknown);
+
+    if (tag.GetElement() != 0x0000)
+    {
+      BinaryMode mode;
+      std::string bulkDataUri;
+        
+      if (formatter_ == NULL)
+      {
+        mode = BinaryMode_InlineBinary;
+      }
+      else
+      {
+        mode = formatter_->Format(bulkDataUri, parentTags, parentIndexes, tag, vr);
+      }
+
+      if (mode != BinaryMode_Ignore)
+      {
+        Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+        node[KEY_VR] = EnumerationToString(vr);
+
+        switch (mode)
+        {
+          case BinaryMode_BulkDataUri:
+            node[KEY_BULK_DATA_URI] = bulkDataUri;
+            break;
+
+          case BinaryMode_InlineBinary:
+          {
+            std::string tmp(static_cast<const char*>(data), size);
+          
+            std::string base64;
+            Toolbox::EncodeBase64(base64, tmp);
+
+            node[KEY_INLINE_BINARY] = base64;
+            break;
+          }
+
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+      }
+    }
+  }
+
+
+  void DicomWebJsonVisitor::VisitIntegers(const std::vector<DicomTag>& parentTags,
+                                          const std::vector<size_t>& parentIndexes,
+                                          const DicomTag& tag,
+                                          ValueRepresentation vr,
+                                          const std::vector<int64_t>& values)
+  {
+    if (tag.GetElement() != 0x0000 &&
+        vr != ValueRepresentation_NotSupported)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(vr);
+
+      if (!values.empty())
+      {
+        Json::Value content = Json::arrayValue;
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          content.append(FormatInteger(values[i]));
+        }
+
+        node[KEY_VALUE] = content;
+      }
+    }
+  }
+
+  void DicomWebJsonVisitor::VisitDoubles(const std::vector<DicomTag>& parentTags,
+                                         const std::vector<size_t>& parentIndexes,
+                                         const DicomTag& tag,
+                                         ValueRepresentation vr,
+                                         const std::vector<double>& values)
+  {
+    if (tag.GetElement() != 0x0000 &&
+        vr != ValueRepresentation_NotSupported)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(vr);
+
+      if (!values.empty())
+      {
+        Json::Value content = Json::arrayValue;
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          content.append(FormatDouble(values[i]));
+        }
+          
+        node[KEY_VALUE] = content;
+      }
+    }
+  }
+
+  
+  void DicomWebJsonVisitor::VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                            const std::vector<size_t>& parentIndexes,
+                                            const DicomTag& tag,
+                                            const std::vector<DicomTag>& values)
+  {
+    if (tag.GetElement() != 0x0000)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(ValueRepresentation_AttributeTag);
+
+      if (!values.empty())
+      {
+        Json::Value content = Json::arrayValue;
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          content.append(FormatTag(values[i]));
+        }
+          
+        node[KEY_VALUE] = content;
+      }
+    }
+  }
+
+  
+  ITagVisitor::Action
+  DicomWebJsonVisitor::VisitString(std::string& newValue,
+                                   const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   ValueRepresentation vr,
+                                   const std::string& value)
+  {
+    if (tag.GetElement() == 0x0000 ||
+        vr == ValueRepresentation_NotSupported)
+    {
+      return Action_None;
+    }
+    else
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(vr);
+
+      if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        // TODO - The JSON file has an UTF-8 encoding, thus DCMTK
+        // replaces the specific character set with "ISO_IR 192"
+        // (UNICODE UTF-8). It is unclear whether the source
+        // character set should be kept: We thus mimic DCMTK.
+        node[KEY_VALUE].append("ISO_IR 192");
+      }
+      else
+      {
+        std::string truncated;
+        
+        if (!value.empty() &&
+            value[value.size() - 1] == '\0')
+        {
+          truncated = value.substr(0, value.size() - 1);
+        }
+        else
+        {
+          truncated = value;
+        }
+        
+        if (!truncated.empty())
+        {
+          std::vector<std::string> tokens;
+          Toolbox::TokenizeString(tokens, truncated, '\\');
+
+          node[KEY_VALUE] = Json::arrayValue;
+          for (size_t i = 0; i < tokens.size(); i++)
+          {
+            try
+            {
+              switch (vr)
+              {
+                case ValueRepresentation_PersonName:
+                {
+                  Json::Value value = Json::objectValue;
+                  if (!tokens[i].empty())
+                  {
+                    value[KEY_ALPHABETIC] = tokens[i];
+                  }
+                  node[KEY_VALUE].append(value);
+                  break;
+                }
+                  
+                case ValueRepresentation_IntegerString:
+                  if (tokens[i].empty())
+                  {
+                    node[KEY_VALUE].append(Json::nullValue);
+                  }
+                  else
+                  {
+                    int64_t value = boost::lexical_cast<int64_t>(tokens[i]);
+                    node[KEY_VALUE].append(FormatInteger(value));
+                  }
+                  
+                  break;
+              
+                case ValueRepresentation_DecimalString:
+                  if (tokens[i].empty())
+                  {
+                    node[KEY_VALUE].append(Json::nullValue);
+                  }
+                  else
+                  {
+                    double value = boost::lexical_cast<double>(tokens[i]);
+                    node[KEY_VALUE].append(FormatDouble(value));
+                  }
+                  break;
+              
+                default:
+                  if (tokens[i].empty())
+                  {
+                    node[KEY_VALUE].append(Json::nullValue);
+                  }
+                  else
+                  {
+                    node[KEY_VALUE].append(tokens[i]);
+                  }
+                  
+                  break;
+              }
+            }
+            catch (boost::bad_lexical_cast&)
+            {
+              throw OrthancException(ErrorCode_BadFileFormat);
+            }
+          }
+        }
+      }
+    }
+      
+    return Action_None;
+  }
+}
diff -pruN 1.5.3+dfsg-1/Core/DicomParsing/DicomWebJsonVisitor.h 1.5.4+dfsg-1/Core/DicomParsing/DicomWebJsonVisitor.h
--- 1.5.3+dfsg-1/Core/DicomParsing/DicomWebJsonVisitor.h	1970-01-01 00:00:00.000000000 +0000
+++ 1.5.4+dfsg-1/Core/DicomParsing/DicomWebJsonVisitor.h	2019-02-08 14:02:50.000000000 +0000
@@ -0,0 +1,160 @@
+/**
+ * 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_PUGIXML)
+#  error Macro ORTHANC_ENABLE_PUGIXML must be defined to use this file
+#endif
+
+#include "ITagVisitor.h"
+
+#include <json/value.h>
+
+
+namespace Orthanc
+{
+  class DicomWebJsonVisitor : public ITagVisitor
+  {
+  public:
+    enum BinaryMode
+    {
+      BinaryMode_Ignore,
+      BinaryMode_BulkDataUri,
+      BinaryMode_InlineBinary
+    };
+    
+    class IBinaryFormatter : public boost::noncopyable
+    {
+    public:
+      virtual ~IBinaryFormatter()
+      {
+      }
+
+      virtual BinaryMode Format(std::string& bulkDataUri,
+                                const std::vector<DicomTag>& parentTags,
+                                const std::vector<size_t>& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr) = 0;
+    };
+    
+  private:
+    Json::Value        result_;
+    IBinaryFormatter  *formatter_;
+
+    static std::string FormatTag(const DicomTag& tag);
+    
+    Json::Value& CreateNode(const std::vector<DicomTag>& parentTags,
+                            const std::vector<size_t>& parentIndexes,
+                            const DicomTag& tag);
+
+    static Json::Value FormatInteger(int64_t value);
+
+    static Json::Value FormatDouble(double value);
+
+  public:
+    DicomWebJsonVisitor() :
+      formatter_(NULL)
+    {
+      Clear();
+    }
+
+    void SetFormatter(IBinaryFormatter& formatter)
+    {
+      formatter_ = &formatter;
+    }
+    
+    void Clear()
+    {
+      result_ = Json::objectValue;
+    }
+
+    const Json::Value& GetResult() const
+    {
+      return result_;
+    }
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+    void FormatXml(std::string& target) const;
+#endif
+
+    virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   ValueRepresentation vr)
+      ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    const DicomTag& tag)
+      ORTHANC_OVERRIDE;
+
+    virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
+                             const std::vector<size_t>& parentIndexes,
+                             const DicomTag& tag,
+                             ValueRepresentation vr,
+                             const void* data,
+                             size_t size)
+      ORTHANC_OVERRIDE;
+
+    virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::vector<int64_t>& values)
+      ORTHANC_OVERRIDE;
+
+    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
+                              const std::vector<size_t>& parentIndexes,
+                              const DicomTag& tag,
+                              ValueRepresentation vr,
+                              const std::vector<double>& values)
+      ORTHANC_OVERRIDE;
+
+    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 const std::vector<DicomTag>& values)
+      ORTHANC_OVERRIDE;
+
+    virtual Action VisitString(std::string& newValue,
+                               const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::string& value)
+      ORTHANC_OVERRIDE;
+  };
+}
diff -pruN 1.5.3+dfsg-1/Core/DicomParsing/FromDcmtkBridge.cpp 1.5.4+dfsg-1/Core/DicomParsing/FromDcmtkBridge.cpp
--- 1.5.3+dfsg-1/Core/DicomParsing/FromDcmtkBridge.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/DicomParsing/FromDcmtkBridge.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -95,6 +95,11 @@
 #include <dcmtk/dcmdata/dcvrus.h>
 #include <dcmtk/dcmdata/dcvrut.h>
 
+#if DCMTK_VERSION_NUMBER >= 361
+#  include <dcmtk/dcmdata/dcvruc.h>
+#  include <dcmtk/dcmdata/dcvrur.h>
+#endif
+
 #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
 #  include <EmbeddedResources.h>
 #endif
@@ -1289,16 +1294,18 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
       case EVR_OB:
         return ValueRepresentation_OtherByte;
 
-        // Not supported as of DCMTK 3.6.0
-        /*case EVR_OD:
-          return ValueRepresentation_OtherDouble;*/
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_OD:
+          return ValueRepresentation_OtherDouble;
+#endif
 
       case EVR_OF:
         return ValueRepresentation_OtherFloat;
 
-        // Not supported as of DCMTK 3.6.0
-        /*case EVR_OL:
-          return ValueRepresentation_OtherLong;*/
+#if DCMTK_VERSION_NUMBER >= 362
+        case EVR_OL:
+          return ValueRepresentation_OtherLong;
+#endif
 
       case EVR_OW:
         return ValueRepresentation_OtherWord;
@@ -1324,9 +1331,10 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
       case EVR_TM:
         return ValueRepresentation_Time;
 
-        // Not supported as of DCMTK 3.6.0
-        /*case EVR_UC:
-          return ValueRepresentation_UnlimitedCharacters;*/
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UC:
+        return ValueRepresentation_UnlimitedCharacters;
+#endif
 
       case EVR_UI:
         return ValueRepresentation_UniqueIdentifier;
@@ -1337,9 +1345,10 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
       case EVR_UN:
         return ValueRepresentation_Unknown;
 
-        // Not supported as of DCMTK 3.6.0
-        /*case EVR_UR:
-          return ValueRepresentation_UniversalResource;*/
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UR:
+        return ValueRepresentation_UniversalResource;
+#endif
 
       case EVR_US:
         return ValueRepresentation_UnsignedShort;
@@ -1355,7 +1364,14 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
 
   static bool IsBinaryTag(const DcmTag& key)
   {
-    return (key.isUnknownVR() || 
+    return (key.isUnknownVR() ||
+#if DCMTK_VERSION_NUMBER >= 361
+            key.getEVR() == EVR_OD ||
+#endif
+            
+#if DCMTK_VERSION_NUMBER >= 362
+            key.getEVR() == EVR_OL ||
+#endif            
             key.getEVR() == EVR_OB ||
             key.getEVR() == EVR_OF ||
             key.getEVR() == EVR_OW ||
@@ -1382,6 +1398,14 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
        * Binary types, handled above
        **/
     
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_OD:
+#endif            
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case EVR_OL:
+#endif            
+
       case EVR_OB:  // other byte
       case EVR_OF:  // other float
       case EVR_OW:  // other word
@@ -1440,6 +1464,16 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
       case EVR_PN:  // person name
         return new DcmPersonName(key);
 
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UC:  // unlimited characters
+        return new DcmUnlimitedCharacters(key);
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UR:  // URI/URL
+        return new DcmUniversalResourceIdentifierOrLocator(key);
+#endif
+          
         
       /**
        * Numerical types
@@ -1540,7 +1574,29 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
     if (tag.IsPrivate() ||
         IsBinaryTag(key))
     {
-      if (element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good())
+      bool ok;
+
+      switch (key.getEVR())
+      {
+        case EVR_OW:
+          if (decoded->size() % sizeof(Uint16) != 0)
+          {
+            LOG(ERROR) << "A tag with OW VR must have an even number of bytes";
+            ok = false;
+          }
+          else
+          {
+            ok = element.putUint16Array((const Uint16*) decoded->c_str(), decoded->size() / sizeof(Uint16)).good();
+          }
+          
+          break;
+      
+        default:
+          ok = element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good();
+          break;
+      }
+      
+      if (ok)
       {
         return;
       }
@@ -1591,6 +1647,10 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
         case EVR_UT:  // unlimited text
         case EVR_PN:  // person name
         case EVR_UI:  // unique identifier
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_UC:  // unlimited characters
+        case EVR_UR:  // URI/URL
+#endif
         {
           ok = element.putString(decoded->c_str()).good();
           break;
@@ -2164,12 +2224,71 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
                                  const DicomTag& tag,
                                  Encoding encoding)
   {
-    // TODO - Merge this function with ConvertLeafElement()
+    // TODO - Merge this function, that is more recent, with ConvertLeafElement()
 
     assert(element.isLeaf());
 
     DcmEVR evr = element.getTag().getEVR();
-    ValueRepresentation vr = FromDcmtkBridge::Convert(evr);
+
+    
+    /**
+     * Fix the EVR for types internal to DCMTK 
+     **/
+
+    if (evr == EVR_ox)  // OB or OW depending on context
+    {
+      evr = EVR_OB;
+    }
+
+    if (evr == EVR_UNKNOWN ||  // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
+        evr == EVR_UNKNOWN2B)  // used internally for elements with unknown VR with 2-byte length field in explicit VR
+    {
+      evr = EVR_UN;
+    }
+
+    const ValueRepresentation vr = FromDcmtkBridge::Convert(evr);
+
+    
+    /**
+     * Deal with binary data (including PixelData).
+     **/
+
+    if (evr == EVR_OB ||  // other byte
+        evr == EVR_OF ||  // other float
+#if DCMTK_VERSION_NUMBER >= 361
+        evr == EVR_OD ||  // other double
+#endif
+#if DCMTK_VERSION_NUMBER >= 362
+        evr == EVR_OL ||  // other long
+#endif
+        evr == EVR_OW ||  // other word
+        evr == EVR_UN)    // unknown value representation
+    {
+      Uint16* data16 = NULL;
+      Uint8* data = NULL;
+
+      if (evr == EVR_OW &&
+          element.getUint16Array(data16) == EC_Normal)
+      {
+        visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength());
+      }
+      else if (evr != EVR_OW &&
+               element.getUint8Array(data) == EC_Normal)
+      {
+        visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
+      }
+      else
+      {
+        visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+      }
+
+      return;  // We're done
+    }
+
+
+    /**
+     * Deal with plain strings (and convert them to UTF-8)
+     **/
 
     char *c = NULL;
     if (element.isaString() &&
@@ -2215,18 +2334,13 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
     try
     {
       // http://support.dcmtk.org/docs/dcvr_8h-source.html
-      switch (element.getVR())
+      switch (evr)
       {
 
         /**
-         * Deal with binary data (including PixelData).
+         * Plain string values.
          **/
 
-        case EVR_OB:  // other byte
-        case EVR_OF:  // other float
-        case EVR_OW:  // other word
-        case EVR_UN:  // unknown value representation
-        case EVR_ox:  // OB or OW depending on context
         case EVR_DS:  // decimal string
         case EVR_IS:  // integer string
         case EVR_AS:  // age string
@@ -2242,21 +2356,46 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
         case EVR_UT:  // unlimited text
         case EVR_PN:  // person name
         case EVR_UI:  // unique identifier
-        case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
-        case EVR_UNKNOWN2B:  // used internally for elements with unknown VR with 2-byte length field in explicit VR
         {
           Uint8* data = NULL;
 
           if (element.getUint8Array(data) == EC_Normal)
           {
-            visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
+            const Uint32 length = element.getLength();
+            Uint32 l = 0;
+            while (l < length &&
+                   data[l] != 0)
+            {
+              l++;
+            }
+
+            if (l == length)
+            {
+              // Not a null-terminated plain string
+              visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+            }
+            else
+            {
+              std::string ignored;
+              std::string s(reinterpret_cast<const char*>(data), l);
+              ITagVisitor::Action action = visitor.VisitString
+                (ignored, parentTags, parentIndexes, tag, vr,
+                 Toolbox::ConvertToUtf8(s, encoding));
+
+              if (action != ITagVisitor::Action_None)
+              {
+                LOG(WARNING) << "Cannot replace this string tag: "
+                             << FromDcmtkBridge::GetTagName(element)
+                             << " (" << tag.Format() << ")";
+              }
+            }
           }
           else
           {
-            visitor.VisitUnknown(parentTags, parentIndexes, tag, vr);
+            visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
           }
 
-          break;
+          return;
         }
     
         /**
@@ -2265,67 +2404,121 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
       
         case EVR_SL:  // signed long
         {
-          Sint32 f;
-          if (dynamic_cast<DcmSignedLong&>(element).getSint32(f).good())
+          DcmSignedLong& content = dynamic_cast<DcmSignedLong&>(element);
+
+          std::vector<int64_t> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
           {
-            visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
+            Sint32 f;
+            if (content.getSint32(f, i).good())
+            {
+              values.push_back(f);
+            }
           }
 
+          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
         case EVR_SS:  // signed short
         {
-          Sint16 f;
-          if (dynamic_cast<DcmSignedShort&>(element).getSint16(f).good())
+          DcmSignedShort& content = dynamic_cast<DcmSignedShort&>(element);
+
+          std::vector<int64_t> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
           {
-            visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
+            Sint16 f;
+            if (content.getSint16(f, i).good())
+            {
+              values.push_back(f);
+            }
           }
 
+          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
         case EVR_UL:  // unsigned long
         {
-          Uint32 f;
-          if (dynamic_cast<DcmUnsignedLong&>(element).getUint32(f).good())
+          DcmUnsignedLong& content = dynamic_cast<DcmUnsignedLong&>(element);
+
+          std::vector<int64_t> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
           {
-            visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
+            Uint32 f;
+            if (content.getUint32(f, i).good())
+            {
+              values.push_back(f);
+            }
           }
 
+          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
         case EVR_US:  // unsigned short
         {
-          Uint16 f;
-          if (dynamic_cast<DcmUnsignedShort&>(element).getUint16(f).good())
+          DcmUnsignedShort& content = dynamic_cast<DcmUnsignedShort&>(element);
+
+          std::vector<int64_t> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
           {
-            visitor.VisitInteger(parentTags, parentIndexes, tag, vr, f);
+            Uint16 f;
+            if (content.getUint16(f, i).good())
+            {
+              values.push_back(f);
+            }
           }
 
+          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
         case EVR_FL:  // float single-precision
         {
-          Float32 f;
-          if (dynamic_cast<DcmFloatingPointSingle&>(element).getFloat32(f).good())
+          DcmFloatingPointSingle& content = dynamic_cast<DcmFloatingPointSingle&>(element);
+
+          std::vector<double> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
           {
-            visitor.VisitDouble(parentTags, parentIndexes, tag, vr, f);
+            Float32 f;
+            if (content.getFloat32(f, i).good())
+            {
+              values.push_back(f);
+            }
           }
 
+          visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
         case EVR_FD:  // float double-precision
         {
-          Float64 f;
-          if (dynamic_cast<DcmFloatingPointDouble&>(element).getFloat64(f).good())
+          DcmFloatingPointDouble& content = dynamic_cast<DcmFloatingPointDouble&>(element);
+
+          std::vector<double> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
           {
-            visitor.VisitDouble(parentTags, parentIndexes, tag, vr, f);
+            Float64 f;
+            if (content.getFloat64(f, i).good())
+            {
+              values.push_back(f);
+            }
           }
 
+          visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
           break;
         }
 
@@ -2336,13 +2529,23 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
 
         case EVR_AT:
         {
-          DcmTagKey tagKey;
-          if (dynamic_cast<DcmAttributeTag&>(element).getTagVal(tagKey, 0).good())
+          DcmAttributeTag& content = dynamic_cast<DcmAttributeTag&>(element);
+
+          std::vector<DicomTag> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
           {
-            DicomTag t(tagKey.getGroup(), tagKey.getElement());
-            visitor.VisitAttribute(parentTags, parentIndexes, tag, vr, t);
+            DcmTagKey f;
+            if (content.getTagVal(f, i).good())
+            {
+              DicomTag t(f.getGroup(), f.getElement());
+              values.push_back(t);
+            }
           }
 
+          assert(vr == ValueRepresentation_AttributeTag);
+          visitor.VisitAttributes(parentTags, parentIndexes, tag, values);
           break;
         }
 
@@ -2353,12 +2556,14 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
          **/
 
         case EVR_SQ:  // sequence of items
+        {
           return;
-
-
-          /**
-           * Internal to DCMTK.
-           **/ 
+        }
+        
+        
+        /**
+         * Internal to DCMTK.
+         **/ 
 
         case EVR_xs:  // SS or US depending on context
         case EVR_lt:  // US, SS or OW depending on context, used for LUT Data (thus the name)
@@ -2374,13 +2579,15 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
         case EVR_pixelItem:  // used internally for pixel items in a compressed image
         case EVR_PixelData:  // used internally for uncompressed pixeld data
         case EVR_OverlayData:  // used internally for overlay data
-          visitor.VisitUnknown(parentTags, parentIndexes, tag, vr);
+        {
+          visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
           return;
+        }
+        
 
-
-          /**
-           * Default case.
-           **/ 
+        /**
+         * Default case.
+         **/ 
 
         default:
           return;
@@ -2418,16 +2625,23 @@ DCMTK_TO_CTYPE_CONVERTER(DcmtkToFloat64C
       // etc. are not." The following dynamic_cast is thus OK.
       DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(element);
 
-      std::vector<DicomTag> tags = parentTags;
-      std::vector<size_t> indexes = parentIndexes;
-      tags.push_back(tag);
-      indexes.push_back(0);
-
-      for (unsigned long i = 0; i < sequence.card(); i++)
+      if (sequence.card() == 0)
       {
-        indexes.back() = static_cast<size_t>(i);
-        DcmItem* child = sequence.getItem(i);
-        ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding);
+        visitor.VisitEmptySequence(parentTags, parentIndexes, tag);
+      }
+      else
+      {
+        std::vector<DicomTag> tags = parentTags;
+        std::vector<size_t> indexes = parentIndexes;
+        tags.push_back(tag);
+        indexes.push_back(0);
+
+        for (unsigned long i = 0; i < sequence.card(); i++)
+        {
+          indexes.back() = static_cast<size_t>(i);
+          DcmItem* child = sequence.getItem(i);
+          ApplyVisitorToDataset(*child, visitor, tags, indexes, encoding);
+        }
       }
     }
   }
diff -pruN 1.5.3+dfsg-1/Core/DicomParsing/ITagVisitor.h 1.5.4+dfsg-1/Core/DicomParsing/ITagVisitor.h
--- 1.5.3+dfsg-1/Core/DicomParsing/ITagVisitor.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/DicomParsing/ITagVisitor.h	2019-02-08 14:02:50.000000000 +0000
@@ -53,36 +53,46 @@ namespace Orthanc
     {
     }
 
-    virtual void VisitUnknown(const std::vector<DicomTag>& parentTags,
-                              const std::vector<size_t>& parentIndexes,
-                              const DicomTag& tag,
-                              ValueRepresentation vr) = 0;
+    // Visiting a DICOM element that is internal to DCMTK
+    virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   ValueRepresentation vr) = 0;
+
+    // SQ
+    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    const DicomTag& tag) = 0;
 
-    virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
-                             const std::vector<size_t>& parentIndexes,
-                             const DicomTag& tag,
-                             ValueRepresentation vr,
-                             const void* data,
-                             size_t size) = 0;
+    // SL, SS, UL, US
+    virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::vector<int64_t>& values) = 0;
 
-    virtual void VisitInteger(const std::vector<DicomTag>& parentTags,
+    // FL, FD
+    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
                               const std::vector<size_t>& parentIndexes,
                               const DicomTag& tag,
                               ValueRepresentation vr,
-                              int64_t value) = 0;
+                              const std::vector<double>& values) = 0;
 
-    virtual void VisitDouble(const std::vector<DicomTag>& parentTags,
+    // AT
+    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 const std::vector<DicomTag>& values) = 0;
+
+    // OB, OD, OF, OL, OW, UN
+    virtual void VisitBinary(const std::vector<DicomTag>& parentTags,
                              const std::vector<size_t>& parentIndexes,
                              const DicomTag& tag,
                              ValueRepresentation vr,
-                             double value) = 0;
-
-    virtual void VisitAttribute(const std::vector<DicomTag>& parentTags,
-                                const std::vector<size_t>& parentIndexes,
-                                const DicomTag& tag,
-                                ValueRepresentation vr,
-                                const DicomTag& value) = 0;
+                             const void* data,
+                             size_t size) = 0;
 
+    // Visiting an UTF-8 string
     virtual Action VisitString(std::string& newValue,
                                const std::vector<DicomTag>& parentTags,
                                const std::vector<size_t>& parentIndexes,
diff -pruN 1.5.3+dfsg-1/Core/Enumerations.cpp 1.5.4+dfsg-1/Core/Enumerations.cpp
--- 1.5.3+dfsg-1/Core/Enumerations.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/Enumerations.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -59,6 +59,8 @@ namespace Orthanc
   static const char* const MIME_WOFF = "application/x-font-woff";
   static const char* const MIME_XML_2 = "text/xml";
   static const char* const MIME_ZIP = "application/zip";
+  static const char* const MIME_DICOM_WEB_JSON = "application/dicom+json";
+  static const char* const MIME_DICOM_WEB_XML = "application/dicom+xml";
 
   // This function is autogenerated by the script
   // "Resources/GenerateErrorCodes.py"
@@ -1103,6 +1105,16 @@ namespace Orthanc
                 
       case MimeType_Woff:
         return MIME_WOFF;
+
+      case MimeType_PrometheusText:
+        // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
+        return "text/plain; version=0.0.4";
+
+      case MimeType_DicomWebJson:
+        return MIME_DICOM_WEB_JSON;
+                
+      case MimeType_DicomWebXml:
+        return MIME_DICOM_WEB_XML;
                 
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
@@ -1712,6 +1724,14 @@ namespace Orthanc
     {
       return MimeType_Woff;
     }
+    else if (mime == MIME_DICOM_WEB_JSON)
+    {
+      return MimeType_DicomWebJson;
+    }
+    else if (mime == MIME_DICOM_WEB_XML)
+    {
+      return MimeType_DicomWebXml;
+    }
     else
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
diff -pruN 1.5.3+dfsg-1/Core/Enumerations.h 1.5.4+dfsg-1/Core/Enumerations.h
--- 1.5.3+dfsg-1/Core/Enumerations.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/Enumerations.h	2019-02-08 14:02:50.000000000 +0000
@@ -104,8 +104,11 @@ namespace Orthanc
     MimeType_Svg,
     MimeType_WebAssembly,
     MimeType_Xml,
-    MimeType_Woff,  // Web Open Font Format
-    MimeType_Zip
+    MimeType_Woff,            // Web Open Font Format
+    MimeType_Zip,
+    MimeType_PrometheusText,  // Prometheus text-based exposition format (for metrics)
+    MimeType_DicomWebJson,
+    MimeType_DicomWebXml
   };
 
   
diff -pruN 1.5.3+dfsg-1/Core/FileStorage/StorageAccessor.cpp 1.5.4+dfsg-1/Core/FileStorage/StorageAccessor.cpp
--- 1.5.3+dfsg-1/Core/FileStorage/StorageAccessor.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/FileStorage/StorageAccessor.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -35,16 +35,39 @@
 #include "StorageAccessor.h"
 
 #include "../Compression/ZlibCompressor.h"
+#include "../MetricsRegistry.h"
 #include "../OrthancException.h"
 #include "../Toolbox.h"
-#include "../Toolbox.h"
 
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
 #  include "../HttpServer/HttpStreamTranscoder.h"
 #endif
 
+
+static const std::string METRICS_CREATE = "orthanc_storage_create_duration_ms";
+static const std::string METRICS_READ = "orthanc_storage_read_duration_ms";
+static const std::string METRICS_REMOVE = "orthanc_storage_remove_duration_ms";
+
+
 namespace Orthanc
 {
+  class StorageAccessor::MetricsTimer : public boost::noncopyable
+  {
+  private:
+    std::auto_ptr<MetricsRegistry::Timer>  timer_;
+
+  public:
+    MetricsTimer(StorageAccessor& that,
+                 const std::string& name)
+    {
+      if (that.metrics_ != NULL)
+      {
+        timer_.reset(new MetricsRegistry::Timer(*that.metrics_, name));
+      }
+    }
+  };
+
+
   FileInfo StorageAccessor::Write(const void* data,
                                   size_t size,
                                   FileContentType type,
@@ -64,6 +87,8 @@ namespace Orthanc
     {
       case CompressionType_None:
       {
+        MetricsTimer timer(*this, METRICS_CREATE);
+
         area_.Create(uuid, data, size, type);
         return FileInfo(uuid, type, size, md5);
       }
@@ -82,13 +107,17 @@ namespace Orthanc
           Toolbox::ComputeMD5(compressedMD5, compressed);
         }
 
-        if (compressed.size() > 0)
-        {
-          area_.Create(uuid, &compressed[0], compressed.size(), type);
-        }
-        else
         {
-          area_.Create(uuid, NULL, 0, type);
+          MetricsTimer timer(*this, METRICS_CREATE);
+
+          if (compressed.size() > 0)
+          {
+            area_.Create(uuid, &compressed[0], compressed.size(), type);
+          }
+          else
+          {
+            area_.Create(uuid, NULL, 0, type);
+          }
         }
 
         return FileInfo(uuid, type, size, md5,
@@ -108,6 +137,7 @@ namespace Orthanc
     {
       case CompressionType_None:
       {
+        MetricsTimer timer(*this, METRICS_READ);
         area_.Read(content, info.GetUuid(), info.GetContentType());
         break;
       }
@@ -117,7 +147,12 @@ namespace Orthanc
         ZlibCompressor zlib;
 
         std::string compressed;
-        area_.Read(compressed, info.GetUuid(), info.GetContentType());
+
+        {
+          MetricsTimer timer(*this, METRICS_READ);
+          area_.Read(compressed, info.GetUuid(), info.GetContentType());
+        }
+
         IBufferCompressor::Uncompress(content, zlib, compressed);
         break;
       }
@@ -132,17 +167,19 @@ namespace Orthanc
   }
 
 
-  void StorageAccessor::Read(Json::Value& content,
-                             const FileInfo& info)
+  void StorageAccessor::ReadRaw(std::string& content,
+                                const FileInfo& info)
   {
-    std::string s;
-    Read(s, info);
+    MetricsTimer timer(*this, METRICS_READ);
+    area_.Read(content, info.GetUuid(), info.GetContentType());
+  }
 
-    Json::Reader reader;
-    if (!reader.parse(s, content))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
+
+  void StorageAccessor::Remove(const std::string& fileUuid,
+                               FileContentType type)
+  {
+    MetricsTimer timer(*this, METRICS_REMOVE);
+    area_.Remove(fileUuid, type);
   }
 
 
@@ -151,7 +188,11 @@ namespace Orthanc
                                     const FileInfo& info,
                                     const std::string& mime)
   {
-    area_.Read(sender.GetBuffer(), info.GetUuid(), info.GetContentType());
+    {
+      MetricsTimer timer(*this, METRICS_READ);
+      area_.Read(sender.GetBuffer(), info.GetUuid(), info.GetContentType());
+    }
+
     sender.SetContentType(mime);
 
     const char* extension;
diff -pruN 1.5.3+dfsg-1/Core/FileStorage/StorageAccessor.h 1.5.4+dfsg-1/Core/FileStorage/StorageAccessor.h
--- 1.5.3+dfsg-1/Core/FileStorage/StorageAccessor.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/FileStorage/StorageAccessor.h	2019-02-08 14:02:50.000000000 +0000
@@ -61,14 +61,23 @@
 #include <string>
 #include <boost/noncopyable.hpp>
 #include <stdint.h>
-#include <json/value.h>
 
 namespace Orthanc
 {
+  class MetricsRegistry;
+
+  /**
+   * This class handles the compression/decompression of the raw files
+   * contained in the storage area, and monitors timing metrics (if
+   * enabled).
+   **/
   class StorageAccessor : boost::noncopyable
   {
   private:
-    IStorageArea&  area_;
+    class MetricsTimer;
+
+    IStorageArea&     area_;
+    MetricsRegistry*  metrics_;
 
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
     void SetupSender(BufferHttpSender& sender,
@@ -77,7 +86,16 @@ namespace Orthanc
 #endif
 
   public:
-    StorageAccessor(IStorageArea& area) : area_(area)
+    StorageAccessor(IStorageArea& area) : 
+      area_(area),
+      metrics_(NULL)
+    {
+    }
+
+    StorageAccessor(IStorageArea& area,
+                    MetricsRegistry& metrics) : 
+      area_(area),
+      metrics_(&metrics)
     {
     }
 
@@ -99,12 +117,15 @@ namespace Orthanc
     void Read(std::string& content,
               const FileInfo& info);
 
-    void Read(Json::Value& content,
-              const FileInfo& info);
+    void ReadRaw(std::string& content,
+                 const FileInfo& info);
+
+    void Remove(const std::string& fileUuid,
+                FileContentType type);
 
     void Remove(const FileInfo& info)
     {
-      area_.Remove(info.GetUuid(), info.GetContentType());
+      Remove(info.GetUuid(), info.GetContentType());
     }
 
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
diff -pruN 1.5.3+dfsg-1/Core/HttpServer/HttpOutput.cpp 1.5.4+dfsg-1/Core/HttpServer/HttpOutput.cpp
--- 1.5.3+dfsg-1/Core/HttpServer/HttpOutput.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/HttpServer/HttpOutput.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -46,6 +46,13 @@
 #include <boost/lexical_cast.hpp>
 
 
+#if ORTHANC_ENABLE_CIVETWEB == 1
+#  if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
+#    error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
+#  endif
+#endif
+
+
 namespace Orthanc
 {
   HttpOutput::StateMachine::StateMachine(IHttpOutputStream& stream,
@@ -177,6 +184,10 @@ namespace Orthanc
       {
         s += "Connection: keep-alive\r\n";
       }
+      else
+      {
+        s += "Connection: close\r\n";
+      }
 
       for (std::list<std::string>::const_iterator
              it = headers_.begin(); it != headers_.end(); ++it)
@@ -428,13 +439,28 @@ namespace Orthanc
       throw OrthancException(ErrorCode_NotImplemented,
                              "Multipart answers are not implemented together "
                              "with keep-alive connections if using Mongoose");
-#else
+      
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+#  if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
       // Turn off Keep-Alive for multipart answers
       // https://github.com/civetweb/civetweb/issues/727
       stream_.DisableKeepAlive();
       header += "Connection: close\r\n";
+#  else
+      // The function "mg_disable_keep_alive()" is not available,
+      // let's continue with Keep-Alive. Performance of WADO-RS will
+      // decrease.
+      header += "Connection: keep-alive\r\n";
+#  endif   
+
+#else
+#  error Please support your embedded Web server here
 #endif
     }
+    else
+    {
+      header += "Connection: close\r\n";
+    }
 
     // Possibly add the cookies
     for (std::list<std::string>::const_iterator
diff -pruN 1.5.3+dfsg-1/Core/HttpServer/HttpServer.cpp 1.5.4+dfsg-1/Core/HttpServer/HttpServer.cpp
--- 1.5.3+dfsg-1/Core/HttpServer/HttpServer.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/HttpServer/HttpServer.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -47,6 +47,9 @@
 #elif ORTHANC_ENABLE_CIVETWEB == 1
 #  include <civetweb.h>
 #  define MONGOOSE_USE_CALLBACKS 1
+#  if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
+#    error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
+#  endif
 
 #else
 #  error "Either Mongoose or Civetweb must be enabled to compile this file"
@@ -114,8 +117,18 @@ namespace Orthanc
 #if ORTHANC_ENABLE_MONGOOSE == 1
         throw OrthancException(ErrorCode_NotImplemented,
                                "Only available if using CivetWeb");
+
 #elif ORTHANC_ENABLE_CIVETWEB == 1
+#  if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
         mg_disable_keep_alive(connection_);
+#  else
+#       warning The function "mg_disable_keep_alive()" is not available, DICOMweb might run slowly
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Only available if using a patched version of CivetWeb");
+#  endif
+
+#else
+#  error Please support your embedded Web server here
 #endif
       }
     };
@@ -1189,6 +1202,8 @@ namespace Orthanc
     
     Stop();
     threadsCount_ = threads;
+
+    LOG(INFO) << "The embedded HTTP server will use " << threads << " threads";
   }
 
 
diff -pruN 1.5.3+dfsg-1/Core/JobsEngine/JobsRegistry.cpp 1.5.4+dfsg-1/Core/JobsEngine/JobsRegistry.cpp
--- 1.5.3+dfsg-1/Core/JobsEngine/JobsRegistry.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/JobsEngine/JobsRegistry.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -1379,7 +1379,18 @@ namespace Orthanc
     for (Json::Value::Members::const_iterator it = members.begin();
          it != members.end(); ++it)
     {
-      std::auto_ptr<JobHandler> job(new JobHandler(unserializer, s[JOBS][*it], *it));
+      std::auto_ptr<JobHandler> job;
+
+      try
+      {
+        job.reset(new JobHandler(unserializer, s[JOBS][*it], *it));
+      }
+      catch (OrthancException& e)
+      {
+        LOG(WARNING) << "Cannot unserialize one job from previous execution, "
+                     << "skipping it: " << e.What();
+        continue;
+      }
 
       const boost::posix_time::ptime lastChangeTime = job->GetLastStateChangeTime();
 
@@ -1398,4 +1409,49 @@ namespace Orthanc
       }
     }
   }
+
+
+  void JobsRegistry::GetStatistics(unsigned int& pending,
+                                   unsigned int& running,
+                                   unsigned int& success,
+                                   unsigned int& failed)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    pending = 0;
+    running = 0;
+    success = 0;
+    failed = 0;
+    
+    for (JobsIndex::const_iterator it = jobsIndex_.begin();
+         it != jobsIndex_.end(); ++it)
+    {
+      JobHandler& job = *it->second;
+
+      switch (job.GetState())
+      {
+        case JobState_Retry:
+        case JobState_Pending:
+          pending ++;
+          break;
+
+        case JobState_Paused:
+        case JobState_Running:
+          running ++;
+          break;
+          
+        case JobState_Success:
+          success ++;
+          break;
+
+        case JobState_Failure:
+          failed ++;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }    
+  }
 }
diff -pruN 1.5.3+dfsg-1/Core/JobsEngine/JobsRegistry.h 1.5.4+dfsg-1/Core/JobsEngine/JobsRegistry.h
--- 1.5.3+dfsg-1/Core/JobsEngine/JobsRegistry.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/JobsEngine/JobsRegistry.h	2019-02-08 14:02:50.000000000 +0000
@@ -199,6 +199,11 @@ namespace Orthanc
 
     void ResetObserver();
 
+    void GetStatistics(unsigned int& pending,
+                       unsigned int& running,
+                       unsigned int& success,
+                       unsigned int& errors);
+
     class RunningJob : public boost::noncopyable
     {
     private:
diff -pruN 1.5.3+dfsg-1/Core/MetricsRegistry.cpp 1.5.4+dfsg-1/Core/MetricsRegistry.cpp
--- 1.5.3+dfsg-1/Core/MetricsRegistry.cpp	1970-01-01 00:00:00.000000000 +0000
+++ 1.5.4+dfsg-1/Core/MetricsRegistry.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -0,0 +1,328 @@
+/**
+ * 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 "PrecompiledHeaders.h"
+#include "MetricsRegistry.h"
+
+#include "OrthancException.h"
+#include "ChunkedBuffer.h"
+
+namespace Orthanc
+{
+  static const boost::posix_time::ptime GetNow()
+  {
+    return boost::posix_time::microsec_clock::universal_time();
+  }
+
+  class MetricsRegistry::Item
+  {
+  private:
+    MetricsType               type_;
+    boost::posix_time::ptime  time_;
+    bool                      hasValue_;
+    float                     value_;
+    
+    void Touch(float value,
+               const boost::posix_time::ptime& now)
+    {
+      hasValue_ = true;
+      value_ = value;
+      time_ = now;
+    }
+
+    void Touch(float value)
+    {
+      Touch(value, GetNow());
+    }
+
+    void UpdateMax(float value,
+                   int duration)
+    {
+      if (hasValue_)
+      {
+        const boost::posix_time::ptime now = GetNow();
+
+        if (value > value_ ||
+            (now - time_).total_seconds() > duration)
+        {
+          Touch(value, now);
+        }
+      }
+      else
+      {
+        Touch(value);
+      }
+    }
+    
+    void UpdateMin(float value,
+                   int duration)
+    {
+      if (hasValue_)
+      {
+        const boost::posix_time::ptime now = GetNow();
+        
+        if (value < value_ ||
+            (now - time_).total_seconds() > duration)
+        {
+          Touch(value, now);
+        }
+      }
+      else
+      {
+        Touch(value);
+      }
+    }
+
+  public:
+    Item(MetricsType type) :
+    type_(type),
+    hasValue_(false)
+    {
+    }
+
+    MetricsType GetType() const
+    {
+      return type_;
+    }
+
+    void Update(float value)
+    {
+      switch (type_)
+      {
+        case MetricsType_Default:
+          Touch(value);
+          break;
+          
+        case MetricsType_MaxOver10Seconds:
+          UpdateMax(value, 10);
+          break;
+
+        case MetricsType_MaxOver1Minute:
+          UpdateMax(value, 60);
+          break;
+
+        case MetricsType_MinOver10Seconds:
+          UpdateMin(value, 10);
+          break;
+
+        case MetricsType_MinOver1Minute:
+          UpdateMin(value, 60);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+    bool HasValue() const
+    {
+      return hasValue_;
+    }
+
+    const boost::posix_time::ptime& GetTime() const
+    {
+      if (hasValue_)
+      {
+        return time_;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    float GetValue() const
+    {
+      if (hasValue_)
+      {
+        return value_;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+  };
+
+
+  MetricsRegistry::~MetricsRegistry()
+  {
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void MetricsRegistry::SetEnabled(bool enabled)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    enabled_ = enabled;
+  }
+
+
+  void MetricsRegistry::Register(const std::string& name,
+                                 MetricsType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::iterator found = content_.find(name);
+
+    if (found == content_.end())
+    {
+      content_[name] = new Item(type);
+    }
+    else
+    {
+      assert(found->second != NULL);
+
+      // This metrics already exists: Only recreate it if there is a
+      // mismatch in the type of metrics
+      if (found->second->GetType() != type)
+      {
+        delete found->second;
+        found->second = new Item(type);
+      }
+    }    
+  }
+
+
+  void MetricsRegistry::SetValueInternal(const std::string& name,
+                                         float value,
+                                         MetricsType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::iterator found = content_.find(name);
+
+    if (found == content_.end())
+    {
+      std::auto_ptr<Item> item(new Item(type));
+      item->Update(value);
+      content_[name] = item.release();
+    }
+    else
+    {
+      assert(found->second != NULL);
+      found->second->Update(value);
+    }
+  }
+
+
+  MetricsType MetricsRegistry::GetMetricsType(const std::string& name)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::const_iterator found = content_.find(name);
+
+    if (found == content_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return found->second->GetType();
+    }
+  }
+
+
+  void MetricsRegistry::ExportPrometheusText(std::string& s)
+  {
+    // https://www.boost.org/doc/libs/1_69_0/doc/html/date_time/examples.html#date_time.examples.seconds_since_epoch
+    static const boost::posix_time::ptime EPOCH(boost::gregorian::date(1970, 1, 1));
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    s.clear();
+
+    if (!enabled_)
+    {
+      return;
+    }
+
+    ChunkedBuffer buffer;
+
+    for (Content::const_iterator it = content_.begin();
+         it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (it->second->HasValue())
+      {
+        boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH;
+
+        std::string line = (it->first + " " +
+                            boost::lexical_cast<std::string>(it->second->GetValue()) + " " + 
+                            boost::lexical_cast<std::string>(diff.total_milliseconds()) + "\n");
+
+        buffer.AddChunk(line);
+      }
+    }
+
+    buffer.Flatten(s);
+  }
+
+
+  void MetricsRegistry::SharedMetrics::Add(float delta)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    value_ += delta;
+    registry_.SetValue(name_, value_);
+  }
+
+
+  void  MetricsRegistry::Timer::Start()
+  {
+    if (registry_.IsEnabled())
+    {
+      active_ = true;
+      start_ = GetNow();
+    }
+    else
+    {
+      active_ = false;
+    }
+  }
+
+
+  MetricsRegistry::Timer::~Timer()
+  {
+    if (active_)
+    {   
+      boost::posix_time::time_duration diff = GetNow() - start_;
+      registry_.SetValue(name_, diff.total_milliseconds(), type_);
+    }
+  }
+}
diff -pruN 1.5.3+dfsg-1/Core/MetricsRegistry.h 1.5.4+dfsg-1/Core/MetricsRegistry.h
--- 1.5.3+dfsg-1/Core/MetricsRegistry.h	1970-01-01 00:00:00.000000000 +0000
+++ 1.5.4+dfsg-1/Core/MetricsRegistry.h	2019-02-08 14:02:50.000000000 +0000
@@ -0,0 +1,189 @@
+/**
+ * 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_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class MetricsRegistry cannot be used in sandboxed environments
+#endif
+
+#include <boost/thread/mutex.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace Orthanc
+{
+  enum MetricsType
+  {
+    MetricsType_Default,
+    MetricsType_MaxOver10Seconds,
+    MetricsType_MaxOver1Minute,
+    MetricsType_MinOver10Seconds,
+    MetricsType_MinOver1Minute
+  };
+  
+  class MetricsRegistry : public boost::noncopyable
+  {
+  private:
+    class Item;
+
+    typedef std::map<std::string, Item*>   Content;
+
+    bool          enabled_;
+    boost::mutex  mutex_;
+    Content       content_;
+
+    void SetValueInternal(const std::string& name,
+                          float value,
+                          MetricsType type);
+
+  public:
+    MetricsRegistry() :
+      enabled_(true)
+    {
+    }
+
+    ~MetricsRegistry();
+
+    bool IsEnabled() const
+    {
+      return enabled_;
+    }
+
+    void SetEnabled(bool enabled);
+
+    void Register(const std::string& name,
+                  MetricsType type);
+
+    void SetValue(const std::string& name,
+                  float value,
+                  MetricsType type)
+    {
+      // Inlining to avoid loosing time if metrics are disabled
+      if (enabled_)
+      {
+        SetValueInternal(name, value, type);
+      }
+    }
+    
+    void SetValue(const std::string& name,
+                  float value)
+    {
+      SetValue(name, value, MetricsType_Default);
+    }
+
+    MetricsType GetMetricsType(const std::string& name);
+
+    // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
+    void ExportPrometheusText(std::string& s);
+
+
+    class SharedMetrics : public boost::noncopyable
+    {
+    private:
+      boost::mutex      mutex_;
+      MetricsRegistry&  registry_;
+      std::string       name_;
+      float             value_;
+
+    public:
+      SharedMetrics(MetricsRegistry& registry,
+                    const std::string& name,
+                    MetricsType type) :
+        registry_(registry),
+        name_(name),
+        value_(0)
+      {
+      }
+
+      void Add(float delta);
+    };
+
+
+    class ActiveCounter : public boost::noncopyable
+    {
+    private:
+      SharedMetrics&   metrics_;
+
+    public:
+      ActiveCounter(SharedMetrics& metrics) :
+        metrics_(metrics)
+      {
+        metrics_.Add(1);
+      }
+
+      ~ActiveCounter()
+      {
+        metrics_.Add(-1);
+      }
+    };
+
+
+    class Timer : public boost::noncopyable
+    {
+    private:
+      MetricsRegistry&          registry_;
+      std::string               name_;
+      MetricsType               type_;
+      bool                      active_;
+      boost::posix_time::ptime  start_;
+
+      void Start();
+
+    public:
+      Timer(MetricsRegistry& registry,
+            const std::string& name) :
+        registry_(registry),
+        name_(name),
+        type_(MetricsType_MaxOver10Seconds)
+      {
+        Start();
+      }
+
+      Timer(MetricsRegistry& registry,
+            const std::string& name,
+            MetricsType type) :
+        registry_(registry),
+        name_(name),
+        type_(type)
+      {
+        Start();
+      }
+
+      ~Timer();
+    };
+  };
+}
diff -pruN 1.5.3+dfsg-1/Core/TemporaryFile.cpp 1.5.4+dfsg-1/Core/TemporaryFile.cpp
--- 1.5.3+dfsg-1/Core/TemporaryFile.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/TemporaryFile.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -34,6 +34,7 @@
 #include "PrecompiledHeaders.h"
 #include "TemporaryFile.h"
 
+#include "OrthancException.h"
 #include "SystemToolbox.h"
 #include "Toolbox.h"
 
@@ -41,15 +42,25 @@
 
 namespace Orthanc
 {
-  static std::string CreateTemporaryPath(const char* extension)
+  static std::string CreateTemporaryPath(const char* temporaryDirectory,
+                                         const char* extension)
   {
+    boost::filesystem::path dir;
+
+    if (temporaryDirectory == NULL)
+    {
 #if BOOST_HAS_FILESYSTEM_V3 == 1
-    boost::filesystem::path tmpDir = boost::filesystem::temp_directory_path();
+      dir = boost::filesystem::temp_directory_path();
 #elif defined(__linux__)
-    boost::filesystem::path tmpDir("/tmp");
+      dir = "/tmp";
 #else
-#error Support your platform here
+#  error Support your platform here
 #endif
+    }
+    else
+    {
+      dir = temporaryDirectory;
+    }
 
     // We use UUID to create unique path to temporary files
     std::string filename = "Orthanc-" + Orthanc::Toolbox::GenerateUuid();
@@ -59,19 +70,20 @@ namespace Orthanc
       filename.append(extension);
     }
 
-    tmpDir /= filename;
-    return tmpDir.string();
+    dir /= filename;
+    return dir.string();
   }
 
 
   TemporaryFile::TemporaryFile() : 
-    path_(CreateTemporaryPath(NULL))
+    path_(CreateTemporaryPath(NULL, NULL))
   {
   }
 
 
-  TemporaryFile::TemporaryFile(const char* extension) :
-    path_(CreateTemporaryPath(extension))
+  TemporaryFile::TemporaryFile(const std::string& temporaryDirectory,
+                               const std::string& extension) :
+    path_(CreateTemporaryPath(temporaryDirectory.c_str(), extension.c_str()))
   {
   }
 
@@ -84,12 +96,39 @@ namespace Orthanc
 
   void TemporaryFile::Write(const std::string& content)
   {
-    SystemToolbox::WriteFile(content, path_);
+    try
+    {
+      SystemToolbox::WriteFile(content, path_);
+    }
+    catch (OrthancException& e)
+    {
+      throw OrthancException(e.GetErrorCode(),
+                             "Can't create temporary file \"" + path_ +
+                             "\" with " + boost::lexical_cast<std::string>(content.size()) +
+                             " bytes: Check you have write access to the "
+                             "temporary directory and that it is not full");
+    }
   }
 
 
   void TemporaryFile::Read(std::string& content) const
   {
-    SystemToolbox::ReadFile(content, path_);
+    try
+    {
+      SystemToolbox::ReadFile(content, path_);
+    }
+    catch (OrthancException& e)
+    {
+      throw OrthancException(e.GetErrorCode(),
+                             "Can't read temporary file \"" + path_ +
+                             "\": Another process has corrupted the temporary directory");
+    }
+  }
+
+
+  void TemporaryFile::Touch()
+  {
+    std::string empty;
+    Write(empty);
   }
 }
diff -pruN 1.5.3+dfsg-1/Core/TemporaryFile.h 1.5.4+dfsg-1/Core/TemporaryFile.h
--- 1.5.3+dfsg-1/Core/TemporaryFile.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/TemporaryFile.h	2019-02-08 14:02:50.000000000 +0000
@@ -53,7 +53,8 @@ namespace Orthanc
   public:
     TemporaryFile();
 
-    TemporaryFile(const char* extension);
+    TemporaryFile(const std::string& temporaryFolder,
+                  const std::string& extension);
 
     ~TemporaryFile();
 
@@ -65,5 +66,7 @@ namespace Orthanc
     void Write(const std::string& content);
 
     void Read(std::string& content) const;
+
+    void Touch();
   };
 }
diff -pruN 1.5.3+dfsg-1/Core/Toolbox.cpp 1.5.4+dfsg-1/Core/Toolbox.cpp
--- 1.5.3+dfsg-1/Core/Toolbox.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/Toolbox.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -99,7 +99,6 @@ extern "C"
 
 #if ORTHANC_ENABLE_PUGIXML == 1
 #  include "ChunkedBuffer.h"
-#  include <pugixml.hpp>
 #endif
 
 
@@ -1023,11 +1022,16 @@ namespace Orthanc
     decl.append_attribute("version").set_value("1.0");
     decl.append_attribute("encoding").set_value("utf-8");
 
+    XmlToString(target, doc);
+  }
+
+  void Toolbox::XmlToString(std::string& target,
+                            const pugi::xml_document& source)
+  {
     ChunkedBufferWriter writer;
-    doc.save(writer, "  ", pugi::format_default, pugi::encoding_utf8);
+    source.save(writer, "  ", pugi::format_default, pugi::encoding_utf8);
     writer.Flatten(target);
   }
-
 #endif
 
 
diff -pruN 1.5.3+dfsg-1/Core/Toolbox.h 1.5.4+dfsg-1/Core/Toolbox.h
--- 1.5.3+dfsg-1/Core/Toolbox.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/Toolbox.h	2019-02-08 14:02:50.000000000 +0000
@@ -68,6 +68,10 @@
  **/
 
 
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include <pugixml.hpp>
+#endif
+
 
 namespace Orthanc
 {
@@ -192,6 +196,11 @@ namespace Orthanc
                    const std::string& arrayElement = "item");
 #endif
 
+#if ORTHANC_ENABLE_PUGIXML == 1
+    void XmlToString(std::string& target,
+                     const pugi::xml_document& source);
+#endif
+
     bool IsInteger(const std::string& str);
 
     void CopyJsonWithoutComments(Json::Value& target,
diff -pruN 1.5.3+dfsg-1/Core/WebServiceParameters.cpp 1.5.4+dfsg-1/Core/WebServiceParameters.cpp
--- 1.5.3+dfsg-1/Core/WebServiceParameters.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/WebServiceParameters.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -502,4 +502,47 @@ namespace Orthanc
     }
   }
 #endif
+
+
+  void WebServiceParameters::FormatPublic(Json::Value& target) const
+  {
+    target = Json::objectValue;
+
+    // Only return the public information identifying the destination.
+    // "Security"-related information such as passwords and HTTP
+    // headers are shown as "null" values.
+    target[KEY_URL] = url_;
+
+    if (!username_.empty())
+    {
+      target[KEY_USERNAME] = username_;
+      target[KEY_PASSWORD] = Json::nullValue;
+    }
+
+    if (!certificateFile_.empty())
+    {
+      target[KEY_CERTIFICATE_FILE] = certificateFile_;
+      target[KEY_CERTIFICATE_KEY_FILE] = Json::nullValue;
+      target[KEY_CERTIFICATE_KEY_PASSWORD] = Json::nullValue;      
+    }
+
+    target[KEY_PKCS11] = pkcs11Enabled_;
+
+    Json::Value headers = Json::arrayValue;
+      
+    for (Dictionary::const_iterator it = headers_.begin();
+         it != headers_.end(); ++it)
+    {
+      // Only list the HTTP headers, not their value
+      headers.append(it->first);
+    }
+
+    target[KEY_HTTP_HEADERS] = headers;
+
+    for (Dictionary::const_iterator it = userProperties_.begin();
+         it != userProperties_.end(); ++it)
+    {
+      target[it->first] = it->second;
+    }
+  }
 }
diff -pruN 1.5.3+dfsg-1/Core/WebServiceParameters.h 1.5.4+dfsg-1/Core/WebServiceParameters.h
--- 1.5.3+dfsg-1/Core/WebServiceParameters.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Core/WebServiceParameters.h	2019-02-08 14:02:50.000000000 +0000
@@ -175,5 +175,7 @@ namespace Orthanc
 #if ORTHANC_SANDBOXED == 0
     void CheckClientCertificate() const;
 #endif
+
+    void FormatPublic(Json::Value& target) const;
   };
 }
diff -pruN 1.5.3+dfsg-1/debian/changelog 1.5.4+dfsg-1/debian/changelog
--- 1.5.3+dfsg-1/debian/changelog	2019-01-26 06:59:21.000000000 +0000
+++ 1.5.4+dfsg-1/debian/changelog	2019-02-09 09:42:35.000000000 +0000
@@ -1,3 +1,9 @@
+orthanc (1.5.4+dfsg-1) unstable; urgency=medium
+
+  * New upstream version
+
+ -- Sebastien Jodogne <s.jodogne@gmail.com>  Sat, 09 Feb 2019 10:42:35 +0100
+
 orthanc (1.5.3+dfsg-1) unstable; urgency=medium
 
   * New upstream version
diff -pruN 1.5.3+dfsg-1/debian/configuration/orthanc.json 1.5.4+dfsg-1/debian/configuration/orthanc.json
--- 1.5.3+dfsg-1/debian/configuration/orthanc.json	2019-01-26 06:59:21.000000000 +0000
+++ 1.5.4+dfsg-1/debian/configuration/orthanc.json	2019-02-09 09:42:35.000000000 +0000
@@ -17,6 +17,18 @@
   // a RAM-drive or a SSD device for performance reasons.
   "IndexDirectory" : "/var/lib/orthanc/db-v6",
 
+  // Path to the directory where Orthanc stores its large temporary
+  // files. The content of this folder can be safely deleted if
+  // Orthanc once stopped. The folder must exist. The corresponding
+  // filesystem must be properly sized, given that for instance a ZIP
+  // archive of DICOM images created by a job can weight several GBs,
+  // and that there might be up to "min(JobsHistorySize,
+  // MediaArchiveSize)" archives to be stored simultaneously. If not
+  // set, Orthanc will use the default temporary folder of the
+  // operating system (such as "/tmp/" on UNIX-like systems, or
+  // "C:/Temp" on Microsoft Windows).
+  // "TemporaryDirectory" : "/tmp/Orthanc/",
+
   // Enable the transparent compression of the DICOM instances
   "StorageCompression" : false,
 
@@ -364,6 +376,9 @@
   // caveats: https://eklitzke.org/the-caveats-of-tcp-nodelay
   "TcpNoDelay" : true,
 
+  // Number of threads that are used by the embedded HTTP server.
+  "HttpThreadsCount" : 50,
+
   // 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
   // this option might prevent the upgrade to newer versions of Orthanc.
@@ -477,5 +492,11 @@
   // answers, but not to filter the DICOM resources (balance between
   // the two modes). By default, the mode is "Always", which
   // corresponds to the behavior of Orthanc <= 1.5.0.
-  "StorageAccessOnFind" : "Always"
+  "StorageAccessOnFind" : "Always",
+
+  // Whether Orthanc monitors its metrics (new in Orthanc 1.5.4). If
+  // set to "true", the metrics can be retrieved at
+  // "/tools/metrics-prometheus" formetted using the Prometheus
+  // text-based exposition format.
+  "MetricsEnabled" : true
 }
diff -pruN 1.5.3+dfsg-1/debian/docs/Orthanc.1 1.5.4+dfsg-1/debian/docs/Orthanc.1
--- 1.5.3+dfsg-1/debian/docs/Orthanc.1	2019-01-26 06:59:21.000000000 +0000
+++ 1.5.4+dfsg-1/debian/docs/Orthanc.1	2019-02-09 09:42:35.000000000 +0000
@@ -1,5 +1,5 @@
 .\" DO NOT MODIFY THIS FILE!  It was generated by help2man 1.47.8.
-.TH ORTHANC "1" "January 2019" "Orthanc 1.5.3" "User Commands"
+.TH ORTHANC "1" "February 2019" "Orthanc 1.5.4" "User Commands"
 .SH NAME
 Orthanc \- Lightweight, RESTful DICOM server for healthcare and medical research
 .SH SYNOPSIS
diff -pruN 1.5.3+dfsg-1/debian/docs/OrthancRecoverCompressedFile.8 1.5.4+dfsg-1/debian/docs/OrthancRecoverCompressedFile.8
--- 1.5.3+dfsg-1/debian/docs/OrthancRecoverCompressedFile.8	2019-01-26 06:59:21.000000000 +0000
+++ 1.5.4+dfsg-1/debian/docs/OrthancRecoverCompressedFile.8	2019-02-09 09:42:35.000000000 +0000
@@ -1,4 +1,4 @@
-.TH ORTHANC "8" "January 2019" "Orthanc 1.5.3" "System Administration tools and Deamons"
+.TH ORTHANC "8" "February 2019" "Orthanc 1.5.4" "System Administration tools and Deamons"
 .SH NAME
 Orthanc \- Lightweight, RESTful DICOM server for healthcare and medical research
 .SH SYNOPSIS
diff -pruN 1.5.3+dfsg-1/.hg_archival.txt 1.5.4+dfsg-1/.hg_archival.txt
--- 1.5.3+dfsg-1/.hg_archival.txt	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/.hg_archival.txt	2019-02-08 14:02:50.000000000 +0000
@@ -1,6 +1,6 @@
 repo: 3959d33612ccaadc0d4d707227fbed09ac35e5fe
-node: 6f5e38ec1f120b8c0ac9c7bfaeafd50ef819d863
-branch: Orthanc-1.5.3
+node: 30418468410719d5301689330d1d282ff61e9751
+branch: Orthanc-1.5.4
 latesttag: dcmtk-3.6.1
-latesttagdistance: 677
-changessincelatesttag: 786
+latesttagdistance: 717
+changessincelatesttag: 829
diff -pruN 1.5.3+dfsg-1/INSTALL 1.5.4+dfsg-1/INSTALL
--- 1.5.3+dfsg-1/INSTALL	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/INSTALL	2019-02-08 14:02:50.000000000 +0000
@@ -91,6 +91,20 @@ NOTES:
   Visual Studio that do not support C++11
 
 
+Native Windows build with Microsoft Visual Studio 2015, Ninja and QtCreator
+---------------------------------------------------------------------------
+
+Open a Visual Studio 2015 x64 Command Prompt.
+
+# cd [...]\OrthancBuild
+# cmake -G Ninja -DSTATIC_BUILD=ON [...]\Orthanc
+# ninja
+
+Then, you can open an existing project in QtCreator:
+* Select the CMakeLists.txt in [...]\Orthanc
+* Import build from [...]\OrthancBuild
+
+
 
 Cross-Compilation for Windows under GNU/Linux
 ---------------------------------------------
diff -pruN 1.5.3+dfsg-1/LinuxCompilation.txt 1.5.4+dfsg-1/LinuxCompilation.txt
--- 1.5.3+dfsg-1/LinuxCompilation.txt	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/LinuxCompilation.txt	2019-02-08 14:02:50.000000000 +0000
@@ -130,6 +130,20 @@ SUPPORTED - Ubuntu 14.04 LTS and 16.04 L
         -DDCMTK_LIBRARIES=dcmjpls \
         -DCMAKE_BUILD_TYPE=Release \
         ~/Orthanc
+# make
+
+
+NB: Instructions to use clang and ninja:
+
+# sudo apt-get install ninja-build
+# CC=/usr/bin/clang CXX=/usr/bin/clang++ cmake -G Ninja \
+        -DALLOW_DOWNLOADS=ON \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
+        -DUSE_SYSTEM_CIVETWEB=OFF \
+        -DDCMTK_LIBRARIES=dcmjpls \
+        -DCMAKE_BUILD_TYPE=Release \
+        ~/Orthanc
+# ninja
 
 
 
diff -pruN 1.5.3+dfsg-1/NEWS 1.5.4+dfsg-1/NEWS
--- 1.5.3+dfsg-1/NEWS	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/NEWS	2019-02-08 14:02:50.000000000 +0000
@@ -2,6 +2,46 @@ Pending changes in the mainline
 ===============================
 
 
+Version 1.5.4 (2019-02-08)
+==========================
+
+General
+-------
+
+* New configuration options:
+  - "MetricsEnabled" to enable the tracking of the metrics of Orthanc
+  - "HttpThreadsCount" to set the number of threads in the embedded HTTP server
+  - "TemporaryDirectory" to set the folder containing the temporary files
+
+REST API
+--------
+
+* API version has been upgraded to 1.4
+* URI "/instances/.../file" can return DICOMweb JSON or XML, depending
+  on the content of the "Accept" HTTP header
+* New URI "/tools/metrics" to dynamically enable/disable the collection of metrics
+* New URI "/tools/metrics-prometheus" to retrieve metrics using Prometheus text format
+* URI "/peers?expand" provides more information about the peers
+
+Plugins
+-------
+
+* New functions in the SDK:
+  - OrthancPluginSetMetricsValue() to set the value of a metrics
+  - OrthancPluginRegisterRefreshMetricsCallback() to ask to refresh metrics
+  - OrthancPluginEncodeDicomWebJson() to convert DICOM to "application/dicom+json"
+  - OrthancPluginEncodeDicomWebXml() to convert DICOM to "application/dicom+xml"
+* New function: 
+* New extensions in the database SDK: LookupResourceAndParent and GetAllMetadata
+
+Maintenance
+-----------
+
+* Fix regression if calling "/tools/find" with the tag "ModalitiesInStudy"
+* Fix build with unpatched versions of Civetweb (missing "mg_disable_keep_alive()")
+* Fix issue #130 (Orthanc failed to start when /tmp partition was full)
+
+
 Version 1.5.3 (2019-01-25)
 ==========================
 
diff -pruN 1.5.3+dfsg-1/OrthancServer/Database/Compatibility/ILookupResourceAndParent.cpp 1.5.4+dfsg-1/OrthancServer/Database/Compatibility/ILookupResourceAndParent.cpp
--- 1.5.3+dfsg-1/OrthancServer/Database/Compatibility/ILookupResourceAndParent.cpp	1970-01-01 00:00:00.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/Database/Compatibility/ILookupResourceAndParent.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -0,0 +1,71 @@
+/**
+ * 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 "ILookupResourceAndParent.h"
+
+#include "../../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    bool ILookupResourceAndParent::Apply(ILookupResourceAndParent& database,
+                                         int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId)
+    {
+      if (!database.LookupResource(id, type, publicId))
+      {
+        return false;
+      }
+      else if (type == ResourceType_Patient)
+      {
+        parentPublicId.clear();
+        return true;
+      }
+      else
+      {
+        int64_t parentId;
+        if (!database.LookupParent(parentId, id))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        parentPublicId = database.GetPublicId(parentId);
+        return true;
+      }
+    }
+  }
+}
diff -pruN 1.5.3+dfsg-1/OrthancServer/Database/Compatibility/ILookupResourceAndParent.h 1.5.4+dfsg-1/OrthancServer/Database/Compatibility/ILookupResourceAndParent.h
--- 1.5.3+dfsg-1/OrthancServer/Database/Compatibility/ILookupResourceAndParent.h	1970-01-01 00:00:00.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/Database/Compatibility/ILookupResourceAndParent.h	2019-02-08 14:02:50.000000000 +0000
@@ -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 "../../ServerEnumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <list>
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class ILookupResourceAndParent : public boost::noncopyable
+    {
+    public:
+      virtual bool LookupResource(int64_t& id,
+                                  ResourceType& type,
+                                  const std::string& publicId) = 0;
+
+      virtual bool LookupParent(int64_t& parentId,
+                                int64_t resourceId) = 0;
+
+      virtual std::string GetPublicId(int64_t resourceId) = 0;
+
+      static bool Apply(ILookupResourceAndParent& database,
+                        int64_t& id,
+                        ResourceType& type,
+                        std::string& parentPublicId,
+                        const std::string& publicId);
+    };
+  }
+}
diff -pruN 1.5.3+dfsg-1/OrthancServer/Database/IDatabaseWrapper.h 1.5.4+dfsg-1/OrthancServer/Database/IDatabaseWrapper.h
--- 1.5.3+dfsg-1/OrthancServer/Database/IDatabaseWrapper.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/Database/IDatabaseWrapper.h	2019-02-08 14:02:50.000000000 +0000
@@ -153,9 +153,6 @@ namespace Orthanc
 
     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;
 
@@ -245,5 +242,15 @@ namespace Orthanc
                                      MetadataType metadata) = 0;
 
     virtual int64_t GetLastChangeIndex() = 0;
+
+
+    /**
+     * Primitives introduced in Orthanc 1.5.4
+     **/
+
+    virtual bool LookupResourceAndParent(int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId) = 0;
   };
 }
diff -pruN 1.5.3+dfsg-1/OrthancServer/Database/SQLiteDatabaseWrapper.cpp 1.5.4+dfsg-1/OrthancServer/Database/SQLiteDatabaseWrapper.cpp
--- 1.5.3+dfsg-1/OrthancServer/Database/SQLiteDatabaseWrapper.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/Database/SQLiteDatabaseWrapper.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -761,21 +761,6 @@ namespace Orthanc
   }
 
 
-  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)
   {
diff -pruN 1.5.3+dfsg-1/OrthancServer/Database/SQLiteDatabaseWrapper.h 1.5.4+dfsg-1/OrthancServer/Database/SQLiteDatabaseWrapper.h
--- 1.5.3+dfsg-1/OrthancServer/Database/SQLiteDatabaseWrapper.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/Database/SQLiteDatabaseWrapper.h	2019-02-08 14:02:50.000000000 +0000
@@ -38,6 +38,7 @@
 #include "../../Core/SQLite/Connection.h"
 #include "Compatibility/ICreateInstance.h"
 #include "Compatibility/IGetChildrenMetadata.h"
+#include "Compatibility/ILookupResourceAndParent.h"
 #include "Compatibility/ISetResourcesContent.h"
 
 namespace Orthanc
@@ -56,6 +57,7 @@ namespace Orthanc
     public IDatabaseWrapper,
     public Compatibility::ICreateInstance,
     public Compatibility::IGetChildrenMetadata,
+    public Compatibility::ILookupResourceAndParent,
     public Compatibility::ISetResourcesContent
   {
   private:
@@ -224,10 +226,6 @@ namespace Orthanc
                                 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;
@@ -361,5 +359,14 @@ namespace Orthanc
     virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
 
     virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
+
+    virtual bool LookupResourceAndParent(int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId)
+      ORTHANC_OVERRIDE
+    {
+      return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId);
+    }
   };
 }
diff -pruN 1.5.3+dfsg-1/OrthancServer/LuaScripting.cpp 1.5.4+dfsg-1/OrthancServer/LuaScripting.cpp
--- 1.5.3+dfsg-1/OrthancServer/LuaScripting.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/LuaScripting.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -164,23 +164,37 @@ namespace Orthanc
         }
       }
       
-      Json::Value tags, metadata;
-      if (that.context_.GetIndex().LookupResource(tags, change_.GetPublicId(), change_.GetResourceType()) &&
-          that.context_.GetIndex().GetMetadata(metadata, change_.GetPublicId()))
+      Json::Value tags;
+      
+      if (that.context_.GetIndex().LookupResource(tags, change_.GetPublicId(), change_.GetResourceType()))
       {
-        LuaScripting::Lock lock(that);
+        std::map<MetadataType, std::string> metadata;
+        that.context_.GetIndex().GetAllMetadata(metadata, change_.GetPublicId());
+        
+        Json::Value formattedMetadata = Json::objectValue;
+
+        for (std::map<MetadataType, std::string>::const_iterator 
+               it = metadata.begin(); it != metadata.end(); ++it)
+        {
+          std::string key = EnumerationToString(it->first);
+          formattedMetadata[key] = it->second;
+        }      
 
-        if (lock.GetLua().IsExistingFunction(name))
         {
-          that.InitializeJob();
+          LuaScripting::Lock lock(that);
+
+          if (lock.GetLua().IsExistingFunction(name))
+          {
+            that.InitializeJob();
 
-          LuaFunctionCall call(lock.GetLua(), name);
-          call.PushString(change_.GetPublicId());
-          call.PushJson(tags["MainDicomTags"]);
-          call.PushJson(metadata);
-          call.Execute();
+            LuaFunctionCall call(lock.GetLua(), name);
+            call.PushString(change_.GetPublicId());
+            call.PushJson(tags["MainDicomTags"]);
+            call.PushJson(formattedMetadata);
+            call.Execute();
 
-          that.SubmitJob();
+            that.SubmitJob();
+          }
         }
       }
     }
diff -pruN 1.5.3+dfsg-1/OrthancServer/main.cpp 1.5.4+dfsg-1/OrthancServer/main.cpp
--- 1.5.3+dfsg-1/OrthancServer/main.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/main.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -814,7 +814,7 @@ static bool StartHttpServer(ServerContex
       httpDescribeErrors = lock.GetConfiguration().GetBooleanParameter("HttpDescribeErrors", true);
   
       // HTTP server
-      //httpServer.SetThreadsCount(50);
+      httpServer.SetThreadsCount(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpThreadsCount", 50));
       httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042));
       httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false));
       httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive));
diff -pruN 1.5.3+dfsg-1/OrthancServer/OrthancConfiguration.cpp 1.5.4+dfsg-1/OrthancServer/OrthancConfiguration.cpp
--- 1.5.3+dfsg-1/OrthancServer/OrthancConfiguration.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/OrthancConfiguration.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -38,6 +38,7 @@
 #include "../Core/Logging.h"
 #include "../Core/OrthancException.h"
 #include "../Core/SystemToolbox.h"
+#include "../Core/TemporaryFile.h"
 #include "../Core/Toolbox.h"
 
 #include "ServerIndex.h"
@@ -47,6 +48,7 @@ static const char* const DICOM_MODALITIE
 static const char* const DICOM_MODALITIES_IN_DB = "DicomModalitiesInDatabase";
 static const char* const ORTHANC_PEERS = "OrthancPeers";
 static const char* const ORTHANC_PEERS_IN_DB = "OrthancPeersInDatabase";
+static const char* const TEMPORARY_DIRECTORY = "TemporaryDirectory";
 
 namespace Orthanc
 {
@@ -826,4 +828,17 @@ namespace Orthanc
   {
     serverIndex_ = NULL;
   }
+
+  
+  TemporaryFile* OrthancConfiguration::CreateTemporaryFile() const
+  {
+    if (json_.isMember(TEMPORARY_DIRECTORY))
+    {
+      return new TemporaryFile(InterpretStringParameterAsPath(GetStringParameter(TEMPORARY_DIRECTORY, ".")), "");
+    }
+    else
+    {
+      return new TemporaryFile;
+    }
+  }
 }
diff -pruN 1.5.3+dfsg-1/OrthancServer/OrthancConfiguration.h 1.5.4+dfsg-1/OrthancServer/OrthancConfiguration.h
--- 1.5.3+dfsg-1/OrthancServer/OrthancConfiguration.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/OrthancConfiguration.h	2019-02-08 14:02:50.000000000 +0000
@@ -47,6 +47,7 @@ namespace Orthanc
 {
   class HttpServer;
   class ServerIndex;
+  class TemporaryFile;
   
   class OrthancConfiguration : public boost::noncopyable
   {
@@ -224,5 +225,7 @@ namespace Orthanc
     void SetServerIndex(ServerIndex& index);
 
     void ResetServerIndex();
+
+    TemporaryFile* CreateTemporaryFile() const;
   };
 }
diff -pruN 1.5.3+dfsg-1/OrthancServer/OrthancFindRequestHandler.cpp 1.5.4+dfsg-1/OrthancServer/OrthancFindRequestHandler.cpp
--- 1.5.3+dfsg-1/OrthancServer/OrthancFindRequestHandler.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/OrthancFindRequestHandler.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -38,6 +38,7 @@
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/Logging.h"
 #include "../Core/Lua/LuaFunctionCall.h"
+#include "../Core/MetricsRegistry.h"
 #include "OrthancConfiguration.h"
 #include "Search/DatabaseLookup.h"
 #include "ServerContext.h"
@@ -551,6 +552,8 @@ namespace Orthanc
                                          const std::string& calledAet,
                                          ModalityManufacturer manufacturer)
   {
+    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_find_scp_duration_ms");
+
     /**
      * Possibly apply the user-supplied Lua filter.
      **/
diff -pruN 1.5.3+dfsg-1/OrthancServer/OrthancMoveRequestHandler.cpp 1.5.4+dfsg-1/OrthancServer/OrthancMoveRequestHandler.cpp
--- 1.5.3+dfsg-1/OrthancServer/OrthancMoveRequestHandler.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/OrthancMoveRequestHandler.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -37,6 +37,7 @@
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/DicomFormat/DicomArray.h"
 #include "../Core/Logging.h"
+#include "../Core/MetricsRegistry.h"
 #include "OrthancConfiguration.h"
 #include "ServerContext.h"
 #include "ServerJobs/DicomModalityStoreJob.h"
@@ -280,6 +281,8 @@ namespace Orthanc
                                                           const std::string& calledAet,
                                                           uint16_t originatorId)
   {
+    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_move_scp_duration_ms");
+
     LOG(WARNING) << "Move-SCU request received for AET \"" << targetAet << "\"";
 
     {
diff -pruN 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestApi.cpp 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestApi.cpp
--- 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -35,6 +35,7 @@
 #include "OrthancRestApi.h"
 
 #include "../../Core/Logging.h"
+#include "../../Core/MetricsRegistry.h"
 #include "../../Core/SerializationToolbox.h"
 #include "../ServerContext.h"
 
@@ -136,7 +137,10 @@ namespace Orthanc
   OrthancRestApi::OrthancRestApi(ServerContext& context) : 
     context_(context),
     leaveBarrier_(false),
-    resetRequestReceived_(false)
+    resetRequestReceived_(false),
+    activeRequests_(context.GetMetricsRegistry(), 
+                    "orthanc_rest_api_active_requests", 
+                    MetricsType_MaxOver10Seconds)
   {
     RegisterSystem();
 
@@ -156,6 +160,25 @@ namespace Orthanc
   }
 
 
+  bool OrthancRestApi::Handle(HttpOutput& output,
+                              RequestOrigin origin,
+                              const char* remoteIp,
+                              const char* username,
+                              HttpMethod method,
+                              const UriComponents& uri,
+                              const Arguments& headers,
+                              const GetArguments& getArguments,
+                              const char* bodyData,
+                              size_t bodySize)
+  {
+    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_rest_api_duration_ms");
+    MetricsRegistry::ActiveCounter counter(activeRequests_);
+
+    return RestApi::Handle(output, origin, remoteIp, username, method,
+                           uri, headers, getArguments, bodyData, bodySize);
+  }
+
+
   ServerContext& OrthancRestApi::GetContext(RestApiCall& call)
   {
     return GetApi(call).context_;
diff -pruN 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestApi.h 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestApi.h
--- 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestApi.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestApi.h	2019-02-08 14:02:50.000000000 +0000
@@ -33,9 +33,10 @@
 
 #pragma once
 
+#include "../../Core/DicomParsing/DicomModification.h"
 #include "../../Core/JobsEngine/SetOfCommandsJob.h"
+#include "../../Core/MetricsRegistry.h"
 #include "../../Core/RestApi/RestApi.h"
-#include "../../Core/DicomParsing/DicomModification.h"
 #include "../ServerEnumerations.h"
 
 #include <set>
@@ -52,9 +53,10 @@ namespace Orthanc
     typedef std::set<std::string> SetOfStrings;
 
   private:
-    ServerContext& context_;
-    bool leaveBarrier_;
-    bool resetRequestReceived_;
+    ServerContext&                  context_;
+    bool                            leaveBarrier_;
+    bool                            resetRequestReceived_;
+    MetricsRegistry::SharedMetrics  activeRequests_;
 
     void RegisterSystem();
 
@@ -75,6 +77,17 @@ namespace Orthanc
   public:
     OrthancRestApi(ServerContext& context);
 
+    virtual bool Handle(HttpOutput& output,
+                        RequestOrigin origin,
+                        const char* remoteIp,
+                        const char* username,
+                        HttpMethod method,
+                        const UriComponents& uri,
+                        const Arguments& headers,
+                        const GetArguments& getArguments,
+                        const char* bodyData,
+                        size_t bodySize) ORTHANC_OVERRIDE;
+
     const bool& LeaveBarrierFlag() const
     {
       return leaveBarrier_;
diff -pruN 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp
--- 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -37,6 +37,7 @@
 #include "../../Core/HttpServer/FilesystemHttpSender.h"
 #include "../../Core/OrthancException.h"
 #include "../../Core/SerializationToolbox.h"
+#include "../OrthancConfiguration.h"
 #include "../ServerContext.h"
 #include "../ServerJobs/ArchiveJob.h"
 
@@ -136,7 +137,13 @@ namespace Orthanc
 
     if (synchronous)
     {
-      boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+      boost::shared_ptr<TemporaryFile> tmp;
+
+      {
+        OrthancConfiguration::ReaderLock lock;
+        tmp.reset(lock.GetConfiguration().CreateTemporaryFile());
+      }
+
       job->SetSynchronousTarget(tmp);
     
       Json::Value publicContent;
diff -pruN 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp
--- 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -1015,16 +1015,9 @@ namespace Orthanc
         
         if (lock.GetConfiguration().LookupOrthancPeer(peer, *it))
         {
-          Json::Value jsonPeer = Json::objectValue;
-          // only return the minimum information to identify the
-          // destination, do not include "security" information like
-          // passwords
-          jsonPeer["Url"] = peer.GetUrl();
-          if (!peer.GetUsername().empty())
-          {
-            jsonPeer["Username"] = peer.GetUsername();
-          }
-          result[*it] = jsonPeer;
+          Json::Value info;
+          peer.FormatPublic(info);
+          result[*it] = info;
         }
       }
       call.GetOutput().AnswerJson(result);
diff -pruN 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestResources.cpp 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestResources.cpp
--- 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -35,6 +35,7 @@
 #include "OrthancRestApi.h"
 
 #include "../../Core/Compression/GzipCompressor.h"
+#include "../../Core/DicomParsing/DicomWebJsonVisitor.h"
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
 #include "../../Core/HttpServer/HttpContentNegociation.h"
@@ -244,6 +245,45 @@ namespace Orthanc
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string publicId = call.GetUriComponent("id", "");
+
+    IHttpHandler::Arguments::const_iterator accept = call.GetHttpHeaders().find("accept");
+    if (accept != call.GetHttpHeaders().end())
+    {
+      // New in Orthanc 1.5.4
+      try
+      {
+        MimeType mime = StringToMimeType(accept->second.c_str());
+
+        if (mime == MimeType_DicomWebJson ||
+            mime == MimeType_DicomWebXml)
+        {
+          DicomWebJsonVisitor visitor;
+          
+          {
+            ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
+            locker.GetDicom().Apply(visitor);
+          }
+
+          if (mime == MimeType_DicomWebJson)
+          {
+            std::string s = visitor.GetResult().toStyledString();
+            call.GetOutput().AnswerBuffer(s, MimeType_DicomWebJson);
+          }
+          else
+          {
+            std::string xml;
+            visitor.FormatXml(xml);
+            call.GetOutput().AnswerBuffer(xml, MimeType_DicomWebXml);
+          }
+          
+          return;
+        }
+      }
+      catch (OrthancException&)
+      {
+      }
+    }
+
     context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom);
   }
 
@@ -645,12 +685,47 @@ namespace Orthanc
   }
 
 
-
   static void GetResourceStatistics(RestApiGetCall& call)
   {
+    static const uint64_t MEGA_BYTES = 1024 * 1024;
+
     std::string publicId = call.GetUriComponent("id", "");
-    Json::Value result;
-    OrthancRestApi::GetIndex(call).GetStatistics(result, publicId);
+
+    ResourceType type;
+    uint64_t diskSize, uncompressedSize, dicomDiskSize, dicomUncompressedSize;
+    unsigned int countStudies, countSeries, countInstances;
+    OrthancRestApi::GetIndex(call).GetResourceStatistics(
+      type, diskSize, uncompressedSize, countStudies, countSeries, 
+      countInstances, dicomDiskSize, dicomUncompressedSize, publicId);
+
+    Json::Value result = Json::objectValue;
+    result["DiskSize"] = boost::lexical_cast<std::string>(diskSize);
+    result["DiskSizeMB"] = static_cast<unsigned int>(diskSize / MEGA_BYTES);
+    result["UncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize);
+    result["UncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
+
+    result["DicomDiskSize"] = boost::lexical_cast<std::string>(dicomDiskSize);
+    result["DicomDiskSizeMB"] = static_cast<unsigned int>(dicomDiskSize / MEGA_BYTES);
+    result["DicomUncompressedSize"] = boost::lexical_cast<std::string>(dicomUncompressedSize);
+    result["DicomUncompressedSizeMB"] = static_cast<unsigned int>(dicomUncompressedSize / MEGA_BYTES);
+
+    switch (type)
+    {
+      // Do NOT add "break" below this point!
+      case ResourceType_Patient:
+        result["CountStudies"] = countStudies;
+
+      case ResourceType_Study:
+        result["CountSeries"] = countSeries;
+
+      case ResourceType_Series:
+        result["CountInstances"] = countInstances;
+
+      case ResourceType_Instance:
+      default:
+        break;
+    }
+
     call.GetOutput().AnswerJson(result);
   }
 
@@ -670,9 +745,9 @@ namespace Orthanc
     CheckValidResourceType(call);
     
     std::string publicId = call.GetUriComponent("id", "");
-    std::list<MetadataType> metadata;
+    std::map<MetadataType, std::string> metadata;
 
-    OrthancRestApi::GetIndex(call).ListAvailableMetadata(metadata, publicId);
+    OrthancRestApi::GetIndex(call).GetAllMetadata(metadata, publicId);
 
     Json::Value result;
 
@@ -680,25 +755,21 @@ namespace Orthanc
     {
       result = Json::objectValue;
       
-      for (std::list<MetadataType>::const_iterator 
+      for (std::map<MetadataType, std::string>::const_iterator 
              it = metadata.begin(); it != metadata.end(); ++it)
       {
-        std::string value;
-        if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, *it))
-        {
-          std::string key = EnumerationToString(*it);
-          result[key] = value;
-        }
+        std::string key = EnumerationToString(it->first);
+        result[key] = it->second;
       }      
     }
     else
     {
       result = Json::arrayValue;
       
-      for (std::list<MetadataType>::const_iterator 
+      for (std::map<MetadataType, std::string>::const_iterator 
              it = metadata.begin(); it != metadata.end(); ++it)
       {       
-        result.append(EnumerationToString(*it));
+        result.append(EnumerationToString(it->first));
       }
     }
 
diff -pruN 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp
--- 1.5.3+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -35,6 +35,7 @@
 #include "OrthancRestApi.h"
 
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/MetricsRegistry.h"
 #include "../../Plugins/Engine/OrthancPlugins.h"
 #include "../../Plugins/Engine/PluginsManager.h"
 #include "../OrthancConfiguration.h"
@@ -93,8 +94,22 @@ namespace Orthanc
 
   static void GetStatistics(RestApiGetCall& call)
   {
+    static const uint64_t MEGA_BYTES = 1024 * 1024;
+
+    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
+    OrthancRestApi::GetIndex(call).GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                                                       countStudies, countSeries, countInstances);
+    
     Json::Value result = Json::objectValue;
-    OrthancRestApi::GetIndex(call).ComputeStatistics(result);
+    result["TotalDiskSize"] = boost::lexical_cast<std::string>(diskSize);
+    result["TotalUncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize);
+    result["TotalDiskSizeMB"] = static_cast<unsigned int>(diskSize / MEGA_BYTES);
+    result["TotalUncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
+    result["CountPatients"] = static_cast<unsigned int>(countPatients);
+    result["CountStudies"] = static_cast<unsigned int>(countStudies);
+    result["CountSeries"] = static_cast<unsigned int>(countSeries);
+    result["CountInstances"] = static_cast<unsigned int>(countInstances);
+
     call.GetOutput().AnswerJson(result);
   }
 
@@ -390,6 +405,76 @@ namespace Orthanc
   }
 
   
+  static void GetMetricsPrometheus(RestApiGetCall& call)
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    OrthancRestApi::GetContext(call).GetPlugins().RefreshMetrics();
+#endif
+
+    static const float MEGA_BYTES = 1024 * 1024;
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
+    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                                           countStudies, countSeries, countInstances);
+
+    unsigned int jobsPending, jobsRunning, jobsSuccess, jobsFailed;
+    context.GetJobsEngine().GetRegistry().GetStatistics(jobsPending, jobsRunning, jobsSuccess, jobsFailed);
+
+    MetricsRegistry& registry = context.GetMetricsRegistry();
+    registry.SetValue("orthanc_disk_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
+    registry.SetValue("orthanc_uncompressed_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
+    registry.SetValue("orthanc_count_patients", static_cast<unsigned int>(countPatients));
+    registry.SetValue("orthanc_count_studies", static_cast<unsigned int>(countStudies));
+    registry.SetValue("orthanc_count_series", static_cast<unsigned int>(countSeries));
+    registry.SetValue("orthanc_count_instances", static_cast<unsigned int>(countInstances));
+    registry.SetValue("orthanc_jobs_pending", jobsPending);
+    registry.SetValue("orthanc_jobs_running", jobsRunning);
+    registry.SetValue("orthanc_jobs_completed", jobsSuccess + jobsFailed);
+    registry.SetValue("orthanc_jobs_success", jobsSuccess);
+    registry.SetValue("orthanc_jobs_failed", jobsFailed);
+    
+    std::string s;
+    registry.ExportPrometheusText(s);
+
+    call.GetOutput().AnswerBuffer(s, MimeType_PrometheusText);
+  }
+
+
+  static void GetMetricsEnabled(RestApiGetCall& call)
+  {
+    bool enabled = OrthancRestApi::GetContext(call).GetMetricsRegistry().IsEnabled();
+    call.GetOutput().AnswerBuffer(enabled ? "1" : "0", MimeType_PlainText);
+  }
+
+
+  static void PutMetricsEnabled(RestApiPutCall& call)
+  {
+    bool enabled;
+
+    std::string body(call.GetBodyData());
+
+    if (body == "1")
+    {
+      enabled = true;
+    }
+    else if (body == "0")
+    {
+      enabled = false;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "The HTTP body must be 0 or 1, but found: " + body);
+    }
+
+    // Success
+    OrthancRestApi::GetContext(call).GetMetricsRegistry().SetEnabled(enabled);
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+
+
   void OrthancRestApi::RegisterSystem()
   {
     Register("/", ServeRoot);
@@ -402,6 +487,9 @@ namespace Orthanc
     Register("/tools/dicom-conformance", GetDicomConformanceStatement);
     Register("/tools/default-encoding", GetDefaultEncoding);
     Register("/tools/default-encoding", SetDefaultEncoding);
+    Register("/tools/metrics", GetMetricsEnabled);
+    Register("/tools/metrics", PutMetricsEnabled);
+    Register("/tools/metrics-prometheus", GetMetricsPrometheus);
 
     Register("/plugins", ListPlugins);
     Register("/plugins/{id}", GetPlugin);
diff -pruN 1.5.3+dfsg-1/OrthancServer/Search/DatabaseLookup.cpp 1.5.4+dfsg-1/OrthancServer/Search/DatabaseLookup.cpp
--- 1.5.3+dfsg-1/OrthancServer/Search/DatabaseLookup.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/Search/DatabaseLookup.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -172,7 +172,8 @@ namespace Orthanc
                       (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive, mandatoryTag));
       }
     }
-    else if (dicomQuery.find('\\') != std::string::npos)
+    else if (tag == DICOM_TAG_MODALITIES_IN_STUDY ||
+             dicomQuery.find('\\') != std::string::npos)
     {
       DicomTag fixedTag(tag);
 
diff -pruN 1.5.3+dfsg-1/OrthancServer/ServerContext.cpp 1.5.4+dfsg-1/OrthancServer/ServerContext.cpp
--- 1.5.3+dfsg-1/OrthancServer/ServerContext.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/ServerContext.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -41,6 +41,7 @@
 #include "../Core/HttpServer/HttpStreamTranscoder.h"
 #include "../Core/JobsEngine/SetOfInstancesJob.h"
 #include "../Core/Logging.h"
+#include "../Core/MetricsRegistry.h"
 #include "../Plugins/Engine/OrthancPlugins.h"
 
 #include "OrthancConfiguration.h"
@@ -237,7 +238,8 @@ namespace Orthanc
 #endif
     done_(false),
     haveJobsChanged_(false),
-    isJobsEngineUnserialized_(false)
+    isJobsEngineUnserialized_(false),
+    metricsRegistry_(new MetricsRegistry)
   {
     {
       OrthancConfiguration::ReaderLock lock;
@@ -249,6 +251,7 @@ namespace Orthanc
       defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC");
       jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2));
       saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true);
+      metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true));
     }
 
     jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
@@ -319,7 +322,8 @@ namespace Orthanc
   void ServerContext::RemoveFile(const std::string& fileUuid,
                                  FileContentType type)
   {
-    area_.Remove(fileUuid, type);
+    StorageAccessor accessor(area_, GetMetricsRegistry());
+    accessor.Remove(fileUuid, type);
   }
 
 
@@ -328,7 +332,8 @@ namespace Orthanc
   {
     try
     {
-      StorageAccessor accessor(area_);
+      MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms");
+      StorageAccessor accessor(area_, GetMetricsRegistry());
 
       resultPublicId = dicom.GetHasher().HashInstance();
 
@@ -469,7 +474,7 @@ namespace Orthanc
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    StorageAccessor accessor(area_);
+    StorageAccessor accessor(area_, GetMetricsRegistry());
     accessor.AnswerFile(output, attachment, GetFileContentMime(content));
   }
 
@@ -497,7 +502,7 @@ namespace Orthanc
 
     std::string content;
 
-    StorageAccessor accessor(area_);
+    StorageAccessor accessor(area_, GetMetricsRegistry());
     accessor.Read(content, attachment);
 
     FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(),
@@ -613,15 +618,18 @@ namespace Orthanc
                              " of instance " + instancePublicId);
     }
 
+    assert(attachment.GetContentType() == content);
+
     if (uncompressIfNeeded)
     {
       ReadAttachment(result, attachment);
     }
     else
     {
-      // Do not interpret the content of the storage area, return the
+      // Do not uncompress the content of the storage area, return the
       // raw data
-      area_.Read(result, attachment.GetUuid(), content);
+      StorageAccessor accessor(area_, GetMetricsRegistry());
+      accessor.ReadRaw(result, attachment);
     }
   }
 
@@ -630,7 +638,7 @@ namespace Orthanc
                                      const FileInfo& attachment)
   {
     // This will decompress the attachment
-    StorageAccessor accessor(area_);
+    StorageAccessor accessor(area_, GetMetricsRegistry());
     accessor.Read(result, attachment);
   }
 
@@ -680,7 +688,7 @@ namespace Orthanc
     // TODO Should we use "gzip" instead?
     CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
 
-    StorageAccessor accessor(area_);
+    StorageAccessor accessor(area_, GetMetricsRegistry());
     FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
 
     StoreStatus status = index_.AddAttachment(attachment, resourceId);
@@ -826,11 +834,13 @@ namespace Orthanc
 
     std::vector<std::string> resources, instances;
 
-    const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);      
-    GetIndex().ApplyLookupResources(resources, &instances, lookup, queryLevel, lookupLimit);
+    {
+      const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);      
+      GetIndex().ApplyLookupResources(resources, &instances, lookup, queryLevel, lookupLimit);
+    }
 
     bool complete = (databaseLimit == 0 ||
-                     resources.size() > databaseLimit);
+                     resources.size() <= databaseLimit);
 
     LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size();
 
diff -pruN 1.5.3+dfsg-1/OrthancServer/ServerContext.h 1.5.4+dfsg-1/OrthancServer/ServerContext.h
--- 1.5.3+dfsg-1/OrthancServer/ServerContext.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/ServerContext.h	2019-02-08 14:02:50.000000000 +0000
@@ -46,6 +46,7 @@ namespace Orthanc
   class DicomInstanceToStore;
   class IStorageArea;
   class JobsEngine;
+  class MetricsRegistry;
   class OrthancPlugins;
   class ParsedDicomFile;
   class RestApiOutput;
@@ -218,6 +219,8 @@ namespace Orthanc
     OrthancHttpHandler  httpHandler_;
     bool saveJobs_;
 
+    std::auto_ptr<MetricsRegistry>  metricsRegistry_;
+
   public:
     class DicomCacheLocker : public boost::noncopyable
     {
@@ -394,5 +397,10 @@ namespace Orthanc
     void SignalUpdatedModalities();
 
     void SignalUpdatedPeers();
+
+    MetricsRegistry& GetMetricsRegistry()
+    {
+      return *metricsRegistry_;
+    }
   };
 }
diff -pruN 1.5.3+dfsg-1/OrthancServer/ServerIndex.cpp 1.5.4+dfsg-1/OrthancServer/ServerIndex.cpp
--- 1.5.3+dfsg-1/OrthancServer/ServerIndex.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/ServerIndex.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -559,12 +559,30 @@ namespace Orthanc
 
 
 
-  bool ServerIndex::GetMetadataAsInteger(int64_t& result,
-                                         int64_t id,
-                                         MetadataType type)
+  static bool LookupStringMetadata(std::string& result,
+                                   const std::map<MetadataType, std::string>& metadata,
+                                   MetadataType type)
+  {
+    std::map<MetadataType, std::string>::const_iterator found = metadata.find(type);
+
+    if (found == metadata.end())
+    {
+      return false;
+    }
+    else
+    {
+      result = found->second;
+      return true;
+    }
+  }
+
+
+  static bool LookupIntegerMetadata(int64_t& result,
+                                    const std::map<MetadataType, std::string>& metadata,
+                                    MetadataType type)
   {
     std::string s;
-    if (!db_.LookupMetadata(s, id, type))
+    if (!LookupStringMetadata(s, metadata, type))
     {
       return false;
     }
@@ -947,10 +965,14 @@ namespace Orthanc
 
   
       // Check whether the series of this new instance is now completed
-      SeriesStatus seriesStatus = GetSeriesStatus(status.seriesId_);
-      if (seriesStatus == SeriesStatus_Complete)
+      int64_t expectedNumberOfInstances;
+      if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary))
       {
-        LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries);
+        SeriesStatus seriesStatus = GetSeriesStatus(status.seriesId_, expectedNumberOfInstances);
+        if (seriesStatus == SeriesStatus_Complete)
+        {
+          LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries);
+        }
       }
       
 
@@ -972,24 +994,21 @@ namespace Orthanc
   }
 
 
-  void ServerIndex::ComputeStatistics(Json::Value& target)
+  void ServerIndex::GetGlobalStatistics(/* out */ uint64_t& diskSize,
+                                        /* out */ uint64_t& uncompressedSize,
+                                        /* out */ uint64_t& countPatients, 
+                                        /* out */ uint64_t& countStudies, 
+                                        /* out */ uint64_t& countSeries, 
+                                        /* out */ uint64_t& countInstances)
   {
     boost::mutex::scoped_lock lock(mutex_);
-    target = Json::objectValue;
-
-    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);
-    target["TotalDiskSizeMB"] = static_cast<unsigned int>(cs / MEGA_BYTES);
-    target["TotalUncompressedSizeMB"] = static_cast<unsigned int>(us / MEGA_BYTES);
-
-    target["CountPatients"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Patient));
-    target["CountStudies"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Study));
-    target["CountSeries"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Series));
-    target["CountInstances"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Instance));
-  }          
-
+    diskSize = db_.GetTotalCompressedSize();
+    uncompressedSize = db_.GetTotalUncompressedSize();
+    countPatients = db_.GetResourceCount(ResourceType_Patient);
+    countStudies = db_.GetResourceCount(ResourceType_Study);
+    countSeries = db_.GetResourceCount(ResourceType_Series);
+    countInstances = db_.GetResourceCount(ResourceType_Instance);
+  }
 
   
   SeriesStatus ServerIndex::GetSeriesStatus(int64_t id,
@@ -1040,21 +1059,6 @@ namespace Orthanc
   }
 
 
-  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)
@@ -1093,22 +1097,27 @@ namespace Orthanc
     // Lookup for the requested resource
     int64_t id;
     ResourceType type;
-    if (!db_.LookupResource(id, type, publicId) ||
+    std::string parent;
+    if (!db_.LookupResourceAndParent(id, type, parent, publicId) ||
         type != expectedType)
     {
       return false;
     }
 
-    // Find the parent resource (if it exists)
-    if (type != ResourceType_Patient)
+    // Set information about the parent resource (if it exists)
+    if (type == ResourceType_Patient)
     {
-      int64_t parentId;
-      if (!db_.LookupParent(parentId, id))
+      if (!parent.empty())
       {
-        throw OrthancException(ErrorCode_InternalError);
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+    else
+    {
+      if (parent.empty())
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
       }
-
-      std::string parent = db_.GetPublicId(parentId);
 
       switch (type)
       {
@@ -1162,6 +1171,10 @@ namespace Orthanc
       }
     }
 
+    // Extract the metadata
+    std::map<MetadataType, std::string> metadata;
+    db_.GetAllMetadata(metadata, id);
+
     // Set the resource type
     switch (type)
     {
@@ -1176,16 +1189,17 @@ namespace Orthanc
       case ResourceType_Series:
       {
         result["Type"] = "Series";
-        result["Status"] = EnumerationToString(GetSeriesStatus(id));
 
         int64_t i;
-        if (GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances))
+        if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances))
         {
           result["ExpectedNumberOfInstances"] = static_cast<int>(i);
+          result["Status"] = EnumerationToString(GetSeriesStatus(id, i));
         }
         else
         {
           result["ExpectedNumberOfInstances"] = Json::nullValue;
+          result["Status"] = EnumerationToString(SeriesStatus_Unknown);
         }
 
         break;
@@ -1205,7 +1219,7 @@ namespace Orthanc
         result["FileUuid"] = attachment.GetUuid();
 
         int64_t i;
-        if (GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries))
+        if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries))
         {
           result["IndexInSeries"] = static_cast<int>(i);
         }
@@ -1227,12 +1241,12 @@ namespace Orthanc
 
     std::string tmp;
 
-    if (db_.LookupMetadata(tmp, id, MetadataType_AnonymizedFrom))
+    if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom))
     {
       result["AnonymizedFrom"] = tmp;
     }
 
-    if (db_.LookupMetadata(tmp, id, MetadataType_ModifiedFrom))
+    if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom))
     {
       result["ModifiedFrom"] = tmp;
     }
@@ -1243,7 +1257,7 @@ namespace Orthanc
     {
       result["IsStable"] = !unstableResources_.Contains(id);
 
-      if (db_.LookupMetadata(tmp, id, MetadataType_LastUpdate))
+      if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate))
       {
         result["LastUpdate"] = tmp;
       }
@@ -1828,19 +1842,19 @@ namespace Orthanc
   }
 
 
-  void ServerIndex::ListAvailableMetadata(std::list<MetadataType>& target,
-                                          const std::string& publicId)
+  void ServerIndex::GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                   const std::string& publicId)
   {
     boost::mutex::scoped_lock lock(mutex_);
 
-    ResourceType rtype;
+    ResourceType type;
     int64_t id;
-    if (!db_.LookupResource(id, rtype, publicId))
+    if (!db_.LookupResource(id, type, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    db_.ListAvailableMetadata(target, id);
+    return db_.GetAllMetadata(target, id);
   }
 
 
@@ -1931,18 +1945,26 @@ namespace Orthanc
   }
 
 
-  void ServerIndex::GetStatisticsInternal(/* out */ uint64_t& diskSize, 
+  void ServerIndex::GetResourceStatistics(/* out */ ResourceType& type,
+                                          /* out */ uint64_t& diskSize, 
                                           /* out */ uint64_t& uncompressedSize, 
                                           /* out */ unsigned int& countStudies, 
                                           /* out */ unsigned int& countSeries, 
                                           /* out */ unsigned int& countInstances, 
                                           /* out */ uint64_t& dicomDiskSize, 
                                           /* out */ uint64_t& dicomUncompressedSize, 
-                                          /* in  */ int64_t id,
-                                          /* in  */ ResourceType type)
+                                          const std::string& publicId)
   {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    int64_t top;
+    if (!db_.LookupResource(top, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
     std::stack<int64_t> toExplore;
-    toExplore.push(id);
+    toExplore.push(top);
 
     countInstances = 0;
     countSeries = 0;
@@ -2023,83 +2045,6 @@ namespace Orthanc
   }
 
 
-
-  void ServerIndex::GetStatistics(Json::Value& target,
-                                  const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t top;
-    if (!db_.LookupResource(top, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    uint64_t uncompressedSize;
-    uint64_t diskSize;
-    uint64_t dicomUncompressedSize;
-    uint64_t dicomDiskSize;
-    unsigned int countStudies;
-    unsigned int countSeries;
-    unsigned int countInstances;
-    GetStatisticsInternal(diskSize, uncompressedSize, countStudies, 
-                          countSeries, countInstances, dicomDiskSize, dicomUncompressedSize, top, type);
-
-    target = Json::objectValue;
-    target["DiskSize"] = boost::lexical_cast<std::string>(diskSize);
-    target["DiskSizeMB"] = static_cast<unsigned int>(diskSize / MEGA_BYTES);
-    target["UncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize);
-    target["UncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
-
-    target["DicomDiskSize"] = boost::lexical_cast<std::string>(dicomDiskSize);
-    target["DicomDiskSizeMB"] = static_cast<unsigned int>(dicomDiskSize / MEGA_BYTES);
-    target["DicomUncompressedSize"] = boost::lexical_cast<std::string>(dicomUncompressedSize);
-    target["DicomUncompressedSizeMB"] = static_cast<unsigned int>(dicomUncompressedSize / MEGA_BYTES);
-
-    switch (type)
-    {
-      // Do NOT add "break" below this point!
-      case ResourceType_Patient:
-        target["CountStudies"] = countStudies;
-
-      case ResourceType_Study:
-        target["CountSeries"] = countSeries;
-
-      case ResourceType_Series:
-        target["CountInstances"] = countInstances;
-
-      case ResourceType_Instance:
-      default:
-        break;
-    }
-  }
-
-
-  void ServerIndex::GetStatistics(/* out */ uint64_t& diskSize, 
-                                  /* out */ uint64_t& uncompressedSize, 
-                                  /* out */ unsigned int& countStudies, 
-                                  /* out */ unsigned int& countSeries, 
-                                  /* out */ unsigned int& countInstances, 
-                                  /* out */ uint64_t& dicomDiskSize, 
-                                  /* out */ uint64_t& dicomUncompressedSize, 
-                                  const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t top;
-    if (!db_.LookupResource(top, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    GetStatisticsInternal(diskSize, uncompressedSize, countStudies, 
-                          countSeries, countInstances, dicomDiskSize,
-                          dicomUncompressedSize, top, type);    
-  }
-
-
   void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
                                                    unsigned int threadSleep)
   {
@@ -2286,41 +2231,6 @@ namespace Orthanc
   }
 
 
-  bool ServerIndex::GetMetadata(Json::Value& target,
-                                const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    target = Json::objectValue;
-
-    ResourceType type;
-    int64_t id;
-    if (!db_.LookupResource(id, type, publicId))
-    {
-      return false;
-    }
-
-    std::list<MetadataType> metadata;
-    db_.ListAvailableMetadata(metadata, id);
-
-    for (std::list<MetadataType>::const_iterator
-           it = metadata.begin(); it != metadata.end(); ++it)
-    {
-      std::string key = EnumerationToString(*it);
-
-      std::string value;
-      if (!db_.LookupMetadata(value, id, *it))
-      {
-        value.clear();
-      }
-
-      target[key] = value;
-    }
-
-    return true;
-  }
-
-
   void ServerIndex::SetGlobalProperty(GlobalProperty property,
                                       const std::string& value)
   {
diff -pruN 1.5.3+dfsg-1/OrthancServer/ServerIndex.h 1.5.4+dfsg-1/OrthancServer/ServerIndex.h
--- 1.5.3+dfsg-1/OrthancServer/ServerIndex.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/ServerIndex.h	2019-02-08 14:02:50.000000000 +0000
@@ -84,8 +84,6 @@ namespace Orthanc
                              int64_t resourceId,
                              ResourceType resourceType);
 
-    SeriesStatus GetSeriesStatus(int64_t id);
-
     bool IsRecyclingNeeded(uint64_t instanceSize);
 
     void Recycle(uint64_t instanceSize,
@@ -97,20 +95,6 @@ namespace Orthanc
                         Orthanc::ResourceType type,
                         const std::string& publicId);
 
-    void GetStatisticsInternal(/* out */ uint64_t& diskSize, 
-                               /* out */ uint64_t& uncompressedSize, 
-                               /* out */ unsigned int& countStudies, 
-                               /* out */ unsigned int& countSeries, 
-                               /* out */ unsigned int& countInstances, 
-                               /* out */ uint64_t& dicomDiskSize, 
-                               /* out */ uint64_t& dicomUncompressedSize, 
-                               /* in  */ int64_t id,
-                               /* in  */ ResourceType type);
-
-    bool GetMetadataAsInteger(int64_t& result,
-                              int64_t id,
-                              MetadataType type);
-
     void LogChange(int64_t internalId,
                    ChangeType changeType,
                    ResourceType resourceType,
@@ -161,7 +145,12 @@ namespace Orthanc
                       DicomInstanceToStore& instance,
                       const Attachments& attachments);
 
-    void ComputeStatistics(Json::Value& target);                        
+    void GetGlobalStatistics(/* out */ uint64_t& diskSize,
+                             /* out */ uint64_t& uncompressedSize,
+                             /* out */ uint64_t& countPatients, 
+                             /* out */ uint64_t& countStudies, 
+                             /* out */ uint64_t& countSeries, 
+                             /* out */ uint64_t& countInstances);
 
     bool LookupResource(Json::Value& result,
                         const std::string& publicId,
@@ -216,16 +205,13 @@ namespace Orthanc
     void DeleteMetadata(const std::string& publicId,
                         MetadataType type);
 
+    void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                        const std::string& publicId);
+
     bool LookupMetadata(std::string& target,
                         const std::string& publicId,
                         MetadataType type);
 
-    void ListAvailableMetadata(std::list<MetadataType>& target,
-                               const std::string& publicId);
-
-    bool GetMetadata(Json::Value& target,
-                     const std::string& publicId);
-
     void ListAvailableAttachments(std::list<FileContentType>& target,
                                   const std::string& publicId,
                                   ResourceType expectedType);
@@ -242,17 +228,15 @@ namespace Orthanc
 
     void DeleteExportedResources();
 
-    void GetStatistics(Json::Value& target,
-                       const std::string& publicId);
-
-    void GetStatistics(/* out */ uint64_t& diskSize, 
-                       /* out */ uint64_t& uncompressedSize, 
-                       /* out */ unsigned int& countStudies, 
-                       /* out */ unsigned int& countSeries, 
-                       /* out */ unsigned int& countInstances, 
-                       /* out */ uint64_t& dicomDiskSize, 
-                       /* out */ uint64_t& dicomUncompressedSize, 
-                       const std::string& publicId);
+    void GetResourceStatistics(/* out */ ResourceType& type,
+                               /* out */ uint64_t& diskSize, 
+                               /* out */ uint64_t& uncompressedSize, 
+                               /* out */ unsigned int& countStudies, 
+                               /* out */ unsigned int& countSeries, 
+                               /* out */ unsigned int& countInstances, 
+                               /* out */ uint64_t& dicomDiskSize, 
+                               /* out */ uint64_t& dicomUncompressedSize, 
+                               const std::string& publicId);
 
     void LookupIdentifierExact(std::vector<std::string>& result,
                                ResourceType level,
diff -pruN 1.5.3+dfsg-1/OrthancServer/ServerJobs/ArchiveJob.cpp 1.5.4+dfsg-1/OrthancServer/ServerJobs/ArchiveJob.cpp
--- 1.5.3+dfsg-1/OrthancServer/ServerJobs/ArchiveJob.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/ServerJobs/ArchiveJob.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -39,6 +39,7 @@
 #include "../../Core/DicomParsing/DicomDirWriter.h"
 #include "../../Core/Logging.h"
 #include "../../Core/OrthancException.h"
+#include "../OrthancConfiguration.h"
 #include "../ServerContext.h"
 
 #include <stdio.h>
@@ -867,13 +868,20 @@ namespace Orthanc
     
     if (synchronousTarget_.get() == NULL)
     {
-      asynchronousTarget_.reset(new TemporaryFile);
+      {
+        OrthancConfiguration::ReaderLock lock;
+        asynchronousTarget_.reset(lock.GetConfiguration().CreateTemporaryFile());
+      }
+
       target = asynchronousTarget_.get();
     }
     else
     {
       target = synchronousTarget_.get();
     }
+
+    assert(target != NULL);
+    target->Touch();  // Make sure we can write to the temporary file
     
     if (writer_.get() != NULL)
     {
diff -pruN 1.5.3+dfsg-1/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp 1.5.4+dfsg-1/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp
--- 1.5.3+dfsg-1/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -43,6 +43,7 @@
 #include "../../../Core/TemporaryFile.h"
 #include "../../../Core/Toolbox.h"
 #include "../../../Core/SystemToolbox.h"
+#include "../../OrthancConfiguration.h"
 
 namespace Orthanc
 {
@@ -92,7 +93,11 @@ namespace Orthanc
         std::string dicom;
         instance.ReadDicom(dicom);
 
-        tmp.reset(new TemporaryFile);
+        {
+          OrthancConfiguration::ReaderLock lock;
+          tmp.reset(lock.GetConfiguration().CreateTemporaryFile());
+        }
+
         tmp->Write(dicom);
         
         arguments.push_back(tmp->GetPath());
diff -pruN 1.5.3+dfsg-1/Plugins/Engine/OrthancPluginDatabase.cpp 1.5.4+dfsg-1/Plugins/Engine/OrthancPluginDatabase.cpp
--- 1.5.3+dfsg-1/Plugins/Engine/OrthancPluginDatabase.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Plugins/Engine/OrthancPluginDatabase.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -135,6 +135,7 @@ namespace Orthanc
     answerDone_ = NULL;
     answerMatchingResources_ = NULL;
     answerMatchingInstances_ = NULL;
+    answerMetadata_ = NULL;
   }
 
 
@@ -270,6 +271,18 @@ namespace Orthanc
       isOptimal = false;
     }
 
+    if (extensions_.getAllMetadata == NULL)
+    {
+      LOG(INFO) << MISSING << "GetAllMetadata()";
+      isOptimal = false;
+    }
+
+    if (extensions_.lookupResourceAndParent == NULL)
+    {
+      LOG(INFO) << MISSING << "LookupResourceAndParent()";
+      isOptimal = false;
+    }
+
     if (isOptimal)
     {
       LOG(INFO) << "The performance of the database index plugin "
@@ -390,21 +403,51 @@ namespace Orthanc
   void OrthancPluginDatabase::GetAllMetadata(std::map<MetadataType, std::string>& target,
                                              int64_t id)
   {
-    std::list<MetadataType> metadata;
-    ListAvailableMetadata(metadata, id);
+    if (extensions_.getAllMetadata == NULL)
+    {
+      // Fallback implementation if extension is missing
+      target.clear();
 
-    target.clear();
+      ResetAnswers();
+      CheckSuccess(backend_.listAvailableMetadata(GetContext(), payload_, id));
 
-    for (std::list<MetadataType>::const_iterator
-           it = metadata.begin(); it != metadata.end(); ++it)
-    {
-      std::string value;
-      if (!LookupMetadata(value, id, *it))
+      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+          type_ != _OrthancPluginDatabaseAnswerType_Int32)
       {
         throw OrthancException(ErrorCode_DatabasePlugin);
       }
 
-      target[*it] = value;
+      target.clear();
+
+      if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
+      {
+        for (std::list<int32_t>::const_iterator 
+               it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
+        {
+          MetadataType type = static_cast<MetadataType>(*it);
+
+          std::string value;
+          if (LookupMetadata(value, id, type))
+          {
+            target[type] = value;
+          }
+        }
+      }
+    }
+    else
+    {
+      ResetAnswers();
+
+      answerMetadata_ = &target;
+      target.clear();
+      
+      CheckSuccess(extensions_.getAllMetadata(GetContext(), payload_, id));
+
+      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+          type_ != _OrthancPluginDatabaseAnswerType_Metadata)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
     }
   }
 
@@ -624,31 +667,6 @@ namespace Orthanc
   }
 
 
-  void OrthancPluginDatabase::ListAvailableMetadata(std::list<MetadataType>& target,
-                                                    int64_t id)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.listAvailableMetadata(GetContext(), payload_, id));
-
-    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-        type_ != _OrthancPluginDatabaseAnswerType_Int32)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    target.clear();
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
-    {
-      for (std::list<int32_t>::const_iterator 
-             it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
-      {
-        target.push_back(static_cast<MetadataType>(*it));
-      }
-    }
-  }
-
-
   void OrthancPluginDatabase::ListAvailableAttachments(std::list<FileContentType>& target,
                                                        int64_t id)
   {
@@ -1020,6 +1038,11 @@ namespace Orthanc
           
           break;
 
+        case _OrthancPluginDatabaseAnswerType_Metadata:
+          assert(answerMetadata_ != NULL);
+          answerMetadata_->clear();
+          break;
+
         default:
           throw OrthancException(ErrorCode_DatabasePlugin,
                                  "Unhandled type of answer for custom index plugin: " +
@@ -1175,6 +1198,24 @@ namespace Orthanc
         break;
       }
 
+      case _OrthancPluginDatabaseAnswerType_Metadata:
+      {
+        const OrthancPluginResourcesContentMetadata& metadata =
+          *reinterpret_cast<const OrthancPluginResourcesContentMetadata*>(answer.valueGeneric);
+
+        MetadataType type = static_cast<MetadataType>(metadata.metadata);
+
+        if (metadata.value == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        assert(answerMetadata_ != NULL &&
+               answerMetadata_->find(type) == answerMetadata_->end());
+        (*answerMetadata_) [type] = metadata.value;
+        break;
+      }
+
       default:
         throw OrthancException(ErrorCode_DatabasePlugin,
                                "Unhandled type of answer for custom index plugin: " +
@@ -1431,4 +1472,63 @@ namespace Orthanc
       CheckSuccess(extensions_.tagMostRecentPatient(payload_, patient));
     }
   }
+
+
+  bool OrthancPluginDatabase::LookupResourceAndParent(int64_t& id,
+                                                      ResourceType& type,
+                                                      std::string& parentPublicId,
+                                                      const std::string& publicId)
+  {
+    if (extensions_.lookupResourceAndParent == NULL)
+    {
+      return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId);
+    }
+    else
+    {
+      std::list<std::string> parent;
+
+      uint8_t isExisting;
+      OrthancPluginResourceType pluginType = OrthancPluginResourceType_Patient;
+      
+      ResetAnswers();
+      CheckSuccess(extensions_.lookupResourceAndParent
+                   (GetContext(), &isExisting, &id, &pluginType, payload_, publicId.c_str()));
+      ForwardAnswers(parent);
+
+      if (isExisting)
+      {
+        type = Plugins::Convert(pluginType);
+
+        if (parent.empty())
+        {
+          if (type != ResourceType_Patient)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+        }
+        else if (parent.size() == 1)
+        {
+          if ((type != ResourceType_Study &&
+               type != ResourceType_Series &&
+               type != ResourceType_Instance) ||
+              parent.front().empty())
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          parentPublicId = parent.front();
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+  }
 }
diff -pruN 1.5.3+dfsg-1/Plugins/Engine/OrthancPluginDatabase.h 1.5.4+dfsg-1/Plugins/Engine/OrthancPluginDatabase.h
--- 1.5.3+dfsg-1/Plugins/Engine/OrthancPluginDatabase.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Plugins/Engine/OrthancPluginDatabase.h	2019-02-08 14:02:50.000000000 +0000
@@ -39,6 +39,7 @@
 #include "../../OrthancServer/Database/Compatibility/ICreateInstance.h"
 #include "../../OrthancServer/Database/Compatibility/IGetChildrenMetadata.h"
 #include "../../OrthancServer/Database/Compatibility/ILookupResources.h"
+#include "../../OrthancServer/Database/Compatibility/ILookupResourceAndParent.h"
 #include "../../OrthancServer/Database/Compatibility/ISetResourcesContent.h"
 #include "../Include/orthanc/OrthancCDatabasePlugin.h"
 #include "PluginsErrorDictionary.h"
@@ -50,12 +51,14 @@ namespace Orthanc
     public Compatibility::ICreateInstance,
     public Compatibility::IGetChildrenMetadata,
     public Compatibility::ILookupResources,
+    public Compatibility::ILookupResourceAndParent,
     public Compatibility::ISetResourcesContent
   {
   private:
     class Transaction;
 
-    typedef std::pair<int64_t, ResourceType>  AnswerResource;
+    typedef std::pair<int64_t, ResourceType>     AnswerResource;
+    typedef std::map<MetadataType, std::string>  AnswerMetadata;
 
     SharedLibrary&  library_;
     PluginsErrorDictionary&  errorDictionary_;
@@ -80,6 +83,7 @@ namespace Orthanc
     bool*                          answerDone_;
     std::list<std::string>*        answerMatchingResources_;
     std::list<std::string>*        answerMatchingInstances_;
+    AnswerMetadata*                answerMetadata_;
 
     OrthancPluginDatabaseContext* GetContext()
     {
@@ -225,10 +229,6 @@ namespace Orthanc
     virtual bool IsProtectedPatient(int64_t internalId) 
       ORTHANC_OVERRIDE;
 
-    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
-                                       int64_t id) 
-      ORTHANC_OVERRIDE;
-
     virtual void ListAvailableAttachments(std::list<FileContentType>& target,
                                           int64_t id) 
       ORTHANC_OVERRIDE;
@@ -364,6 +364,12 @@ namespace Orthanc
     virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
   
     virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
+
+    virtual bool LookupResourceAndParent(int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId)
+      ORTHANC_OVERRIDE;
   };
 }
 
diff -pruN 1.5.3+dfsg-1/Plugins/Engine/OrthancPlugins.cpp 1.5.4+dfsg-1/Plugins/Engine/OrthancPlugins.cpp
--- 1.5.3+dfsg-1/Plugins/Engine/OrthancPlugins.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Plugins/Engine/OrthancPlugins.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -44,29 +44,31 @@
 
 
 #include "../../Core/ChunkedBuffer.h"
+#include "../../Core/Compression/GzipCompressor.h"
+#include "../../Core/Compression/ZlibCompressor.h"
 #include "../../Core/DicomFormat/DicomArray.h"
+#include "../../Core/DicomParsing/DicomWebJsonVisitor.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
+#include "../../Core/DicomParsing/ToDcmtkBridge.h"
 #include "../../Core/HttpServer/HttpToolbox.h"
+#include "../../Core/Images/Image.h"
+#include "../../Core/Images/ImageProcessing.h"
+#include "../../Core/Images/JpegReader.h"
+#include "../../Core/Images/JpegWriter.h"
+#include "../../Core/Images/PngReader.h"
+#include "../../Core/Images/PngWriter.h"
 #include "../../Core/Logging.h"
+#include "../../Core/MetricsRegistry.h"
 #include "../../Core/OrthancException.h"
 #include "../../Core/SerializationToolbox.h"
 #include "../../Core/Toolbox.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/DicomParsing/ToDcmtkBridge.h"
+#include "../../OrthancServer/DefaultDicomImageDecoder.h"
 #include "../../OrthancServer/OrthancConfiguration.h"
+#include "../../OrthancServer/OrthancFindRequestHandler.h"
+#include "../../OrthancServer/Search/HierarchicalMatcher.h"
 #include "../../OrthancServer/ServerContext.h"
 #include "../../OrthancServer/ServerToolbox.h"
-#include "../../OrthancServer/Search/HierarchicalMatcher.h"
-#include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
-#include "../../Core/Compression/ZlibCompressor.h"
-#include "../../Core/Compression/GzipCompressor.h"
-#include "../../Core/Images/Image.h"
-#include "../../Core/Images/PngReader.h"
-#include "../../Core/Images/PngWriter.h"
-#include "../../Core/Images/JpegReader.h"
-#include "../../Core/Images/JpegWriter.h"
-#include "../../Core/Images/ImageProcessing.h"
-#include "../../OrthancServer/DefaultDicomImageDecoder.h"
-#include "../../OrthancServer/OrthancFindRequestHandler.h"
 #include "PluginsEnumerations.h"
 #include "PluginsJob.h"
 
@@ -312,6 +314,94 @@ namespace Orthanc
         return parameters_[i];
       }
     };
+
+
+    class DicomWebBinaryFormatter : public DicomWebJsonVisitor::IBinaryFormatter
+    {
+    private:
+      OrthancPluginDicomWebBinaryCallback  callback_;
+      DicomWebJsonVisitor::BinaryMode      currentMode_;
+      std::string                          currentBulkDataUri_;
+
+      static void Setter(OrthancPluginDicomWebNode*       node,
+                         OrthancPluginDicomWebBinaryMode  mode,
+                         const char*                      bulkDataUri)
+      {
+        DicomWebBinaryFormatter& that = *reinterpret_cast<DicomWebBinaryFormatter*>(node);
+
+        switch (mode)
+        {
+          case OrthancPluginDicomWebBinaryMode_Ignore:
+            that.currentMode_ = DicomWebJsonVisitor::BinaryMode_Ignore;
+            break;
+              
+          case OrthancPluginDicomWebBinaryMode_InlineBinary:
+            that.currentMode_ = DicomWebJsonVisitor::BinaryMode_InlineBinary;
+            break;
+              
+          case OrthancPluginDicomWebBinaryMode_BulkDataUri:
+            if (bulkDataUri == NULL)
+            {
+              throw OrthancException(ErrorCode_NullPointer);
+            }              
+            
+            that.currentBulkDataUri_ = bulkDataUri;
+            that.currentMode_ = DicomWebJsonVisitor::BinaryMode_BulkDataUri;
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+      }
+      
+    public:
+      DicomWebBinaryFormatter(const _OrthancPluginEncodeDicomWeb& parameters) :
+        callback_(parameters.callback)
+      {
+      }
+      
+      virtual DicomWebJsonVisitor::BinaryMode Format(std::string& bulkDataUri,
+                                                     const std::vector<DicomTag>& parentTags,
+                                                     const std::vector<size_t>& parentIndexes,
+                                                     const DicomTag& tag,
+                                                     ValueRepresentation vr)
+      {
+        if (callback_ == NULL)
+        {
+          return DicomWebJsonVisitor::BinaryMode_InlineBinary;
+        }
+        else
+        {
+          assert(parentTags.size() == parentIndexes.size());
+          std::vector<uint16_t> groups(parentTags.size());
+          std::vector<uint16_t> elements(parentTags.size());
+          std::vector<uint32_t> indexes(parentTags.size());
+
+          for (size_t i = 0; i < parentTags.size(); i++)
+          {
+            groups[i] = parentTags[i].GetGroup();
+            elements[i] = parentTags[i].GetElement();
+            indexes[i] = static_cast<uint32_t>(parentIndexes[i]);
+          }
+          bool empty = parentTags.empty();
+
+          currentMode_ = DicomWebJsonVisitor::BinaryMode_Ignore;
+
+          callback_(reinterpret_cast<OrthancPluginDicomWebNode*>(this),
+                    DicomWebBinaryFormatter::Setter,
+                    static_cast<uint32_t>(parentTags.size()),
+                    (empty ? NULL : &groups[0]),
+                    (empty ? NULL : &elements[0]),
+                    (empty ? NULL : &indexes[0]),
+                    tag.GetGroup(),
+                    tag.GetElement(),
+                    Plugins::Convert(vr));
+
+          bulkDataUri = currentBulkDataUri_;          
+          return currentMode_;
+        }
+      }
+    };
   }
 
 
@@ -462,6 +552,7 @@ namespace Orthanc
     typedef std::list<OrthancPluginIncomingHttpRequestFilter2>  IncomingHttpRequestFilters2;
     typedef std::list<OrthancPluginDecodeImageCallback>  DecodeImageCallbacks;
     typedef std::list<OrthancPluginJobsUnserializer>  JobsUnserializers;
+    typedef std::list<OrthancPluginRefreshMetricsCallback>  RefreshMetricsCallbacks;
     typedef std::map<Property, std::string>  Properties;
 
     PluginsManager manager_;
@@ -476,6 +567,7 @@ namespace Orthanc
     _OrthancPluginMoveCallback moveCallbacks_;
     IncomingHttpRequestFilters  incomingHttpRequestFilters_;
     IncomingHttpRequestFilters2 incomingHttpRequestFilters2_;
+    RefreshMetricsCallbacks refreshMetricsCallbacks_;
     std::auto_ptr<StorageAreaFactory>  storageArea_;
 
     boost::recursive_mutex restCallbackMutex_;
@@ -485,6 +577,7 @@ namespace Orthanc
     boost::mutex worklistCallbackMutex_;
     boost::mutex decodeImageCallbackMutex_;
     boost::mutex jobsUnserializersMutex_;
+    boost::mutex refreshMetricsMutex_;
     boost::recursive_mutex invokeServiceMutex_;
 
     Properties properties_;
@@ -904,6 +997,9 @@ namespace Orthanc
         sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
         sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
         sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast<int>(DicomToJsonFlags_IncludeBinary) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast<int>(DicomToJsonFlags_IncludePrivateTags) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast<int>(DicomToJsonFlags_IncludeUnknownTags) ||
@@ -1310,6 +1406,18 @@ namespace Orthanc
   }
 
 
+  void OrthancPlugins::RegisterRefreshMetricsCallback(const void* parameters)
+  {
+    const _OrthancPluginRegisterRefreshMetricsCallback& p = 
+      *reinterpret_cast<const _OrthancPluginRegisterRefreshMetricsCallback*>(parameters);
+
+    boost::mutex::scoped_lock lock(pimpl_->refreshMetricsMutex_);
+
+    LOG(INFO) << "Plugin has registered a callback to refresh its metrics";
+    pimpl_->refreshMetricsCallbacks_.push_back(p.callback);
+  }
+
+
   void OrthancPlugins::AnswerBuffer(const void* parameters)
   {
     const _OrthancPluginAnswerBuffer& p = 
@@ -3096,6 +3204,65 @@ namespace Orthanc
         return true;
       }
 
+      case _OrthancPluginService_SetMetricsValue:
+      {
+        const _OrthancPluginSetMetricsValue& p =
+          *reinterpret_cast<const _OrthancPluginSetMetricsValue*>(parameters);
+
+        MetricsType type;
+        switch (p.type)
+        {
+          case OrthancPluginMetricsType_Default:
+            type = MetricsType_Default;
+            break;
+
+          case OrthancPluginMetricsType_Timer:
+            type = MetricsType_MaxOver10Seconds;
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        
+        {
+          PImpl::ServerContextLock lock(*pimpl_);
+          lock.GetContext().GetMetricsRegistry().SetValue(p.name, p.value, type);
+        }
+
+        return true;
+      }
+
+      case _OrthancPluginService_EncodeDicomWebJson:
+      case _OrthancPluginService_EncodeDicomWebXml:
+      {
+        const _OrthancPluginEncodeDicomWeb& p =
+          *reinterpret_cast<const _OrthancPluginEncodeDicomWeb*>(parameters);
+
+        DicomWebBinaryFormatter formatter(p);
+        
+        DicomWebJsonVisitor visitor;
+        visitor.SetFormatter(formatter);
+
+        {
+          ParsedDicomFile dicom(p.dicom, p.dicomSize);
+          dicom.Apply(visitor);
+        }
+
+        std::string s;
+
+        if (service == _OrthancPluginService_EncodeDicomWebJson)
+        {
+          s = visitor.GetResult().toStyledString();
+        }
+        else
+        {
+          visitor.FormatXml(s);
+        }
+        
+        *p.target = CopyString(s);
+        return true;
+      }
+
       default:
         return false;
     }
@@ -3157,6 +3324,10 @@ namespace Orthanc
         RegisterIncomingHttpRequestFilter2(parameters);
         return true;
 
+      case _OrthancPluginService_RegisterRefreshMetricsCallback:
+        RegisterRefreshMetricsCallback(parameters);
+        return true;
+
       case _OrthancPluginService_RegisterStorageArea:
       {
         LOG(INFO) << "Plugin has registered a custom storage area";
@@ -3659,4 +3830,20 @@ namespace Orthanc
 
     return NULL;
   }
+
+
+  void OrthancPlugins::RefreshMetrics()
+  {
+    boost::mutex::scoped_lock lock(pimpl_->refreshMetricsMutex_);
+
+    for (PImpl::RefreshMetricsCallbacks::iterator 
+           it = pimpl_->refreshMetricsCallbacks_.begin();
+         it != pimpl_->refreshMetricsCallbacks_.end(); ++it)
+    {
+      if (*it != NULL)
+      {
+        (*it) ();
+      }
+    }
+  }
 }
diff -pruN 1.5.3+dfsg-1/Plugins/Engine/OrthancPlugins.h 1.5.4+dfsg-1/Plugins/Engine/OrthancPlugins.h
--- 1.5.3+dfsg-1/Plugins/Engine/OrthancPlugins.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Plugins/Engine/OrthancPlugins.h	2019-02-08 14:02:50.000000000 +0000
@@ -111,6 +111,8 @@ namespace Orthanc
 
     void RegisterIncomingHttpRequestFilter2(const void* parameters);
 
+    void RegisterRefreshMetricsCallback(const void* parameters);
+
     void AnswerBuffer(const void* parameters);
 
     void Redirect(const void* parameters);
@@ -313,6 +315,8 @@ namespace Orthanc
 
     IJob* UnserializeJob(const std::string& type,
                          const Json::Value& value);
+
+    void RefreshMetrics();
   };
 }
 
diff -pruN 1.5.3+dfsg-1/Plugins/Include/orthanc/OrthancCDatabasePlugin.h 1.5.4+dfsg-1/Plugins/Include/orthanc/OrthancCDatabasePlugin.h
--- 1.5.3+dfsg-1/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	2019-02-08 14:02:50.000000000 +0000
@@ -76,6 +76,7 @@ extern "C"
     _OrthancPluginDatabaseAnswerType_Resource = 16,
     _OrthancPluginDatabaseAnswerType_String = 17,
     _OrthancPluginDatabaseAnswerType_MatchingResource = 18,  /* New in Orthanc 1.5.2 */
+    _OrthancPluginDatabaseAnswerType_Metadata = 19,          /* New in Orthanc 1.5.4 */
 
     _OrthancPluginDatabaseAnswerType_INTERNAL = 0x7fffffff
   } _OrthancPluginDatabaseAnswerType;
@@ -335,6 +336,25 @@ extern "C"
     context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
   }
 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerMetadata(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    int64_t                        resourceId,
+    int32_t                        type,
+    const char*                    value)
+  {
+    OrthancPluginResourcesContentMetadata metadata;
+    _OrthancPluginDatabaseAnswer params;
+    metadata.resource = resourceId;
+    metadata.metadata = type;
+    metadata.value = value;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Metadata;
+    params.valueGeneric = &metadata;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
   ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedAttachment(
     OrthancPluginContext*          context,
     OrthancPluginDatabaseContext*  database,
@@ -783,7 +803,6 @@ extern "C"
       OrthancPluginResourceType queryLevel,
       uint32_t limit,
       uint8_t requestSomeInstance);
-
     
     OrthancPluginErrorCode  (*createInstance) (
       /* output */
@@ -825,6 +844,32 @@ extern "C"
       void* payload,
       int64_t patientId);
                    
+    
+    /**
+     * Extensions since Orthanc 1.5.4
+     **/
+
+    /* Ouput: Use OrthancPluginDatabaseAnswerMetadata */
+    OrthancPluginErrorCode  (*getAllMetadata) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t resourceId);
+    
+    /* Ouput: Use OrthancPluginDatabaseAnswerString to send 
+       the public ID of the parent (if the resource is not a patient) */
+    OrthancPluginErrorCode  (*lookupResourceAndParent) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      uint8_t* isExisting,
+      int64_t* id,
+      OrthancPluginResourceType* type,
+      
+      /* inputs */
+      void* payload,
+      const char* publicId);
+
   } OrthancPluginDatabaseExtensions;
 
 /*<! @endcond */
diff -pruN 1.5.3+dfsg-1/Plugins/Include/orthanc/OrthancCPlugin.h 1.5.4+dfsg-1/Plugins/Include/orthanc/OrthancCPlugin.h
--- 1.5.3+dfsg-1/Plugins/Include/orthanc/OrthancCPlugin.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Plugins/Include/orthanc/OrthancCPlugin.h	2019-02-08 14:02:50.000000000 +0000
@@ -24,6 +24,7 @@
  *    - Possibly register a custom decoder for DICOM images using OrthancPluginRegisterDecodeImageCallback().
  *    - Possibly register a callback to filter incoming HTTP requests using OrthancPluginRegisterIncomingHttpRequestFilter2().
  *    - Possibly register a callback to unserialize jobs using OrthancPluginRegisterJobsUnserializer().
+ *    - Possibly register a callback to refresh its metrics using OrthancPluginRegisterRefreshMetricsCallback().
  * -# <tt>void OrthancPluginFinalize()</tt>:
  *    This function is invoked by Orthanc during its shutdown. The plugin
  *    must free all its memory.
@@ -119,7 +120,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     5
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  2
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  4
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -424,6 +425,9 @@ extern "C"
     _OrthancPluginService_GenerateUuid = 28,
     _OrthancPluginService_RegisterPrivateDictionaryTag = 29,
     _OrthancPluginService_AutodetectMimeType = 30,
+    _OrthancPluginService_SetMetricsValue = 31,
+    _OrthancPluginService_EncodeDicomWebJson = 32,
+    _OrthancPluginService_EncodeDicomWebXml = 33,
     
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -437,6 +441,7 @@ extern "C"
     _OrthancPluginService_RegisterFindCallback = 1008,
     _OrthancPluginService_RegisterMoveCallback = 1009,
     _OrthancPluginService_RegisterIncomingHttpRequestFilter2 = 1010,
+    _OrthancPluginService_RegisterRefreshMetricsCallback = 1011,
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -892,6 +897,35 @@ extern "C"
 
 
   /**
+   * The available types of metrics.
+   **/
+  typedef enum
+  {
+    OrthancPluginMetricsType_Default,   /*!< Default metrics */
+
+    /**
+     * This metrics represents a time duration. Orthanc will keep the
+     * maximum value of the metrics over a sliding window of ten
+     * seconds, which is useful if the metrics is sampled frequently.
+     **/
+    OrthancPluginMetricsType_Timer
+  } OrthancPluginMetricsType;
+  
+
+  /**
+   * The available modes to export a binary DICOM tag into a DICOMweb
+   * JSON or XML document.
+   **/
+  typedef enum
+  {
+    OrthancPluginDicomWebBinaryMode_Ignore,        /*!< Don't include binary tags */
+    OrthancPluginDicomWebBinaryMode_InlineBinary,  /*!< Inline encoding using Base64 */
+    OrthancPluginDicomWebBinaryMode_BulkDataUri    /*!< Use a bulk data URI field */
+  } OrthancPluginDicomWebBinaryMode;
+
+  
+
+  /**
    * @brief A memory buffer allocated by the core system of Orthanc.
    *
    * A memory buffer allocated by the core system of Orthanc. When the
@@ -999,6 +1033,15 @@ extern "C"
    **/
   typedef struct _OrthancPluginJob_t OrthancPluginJob;  
 
+
+
+  /**
+   * @brief Opaque structure that represents a node in a JSON or XML
+   * document used in DICOMweb.
+   * @ingroup Toolbox
+   **/
+  typedef struct _OrthancPluginDicomWebNode_t OrthancPluginDicomWebNode;
+
   
 
   /**
@@ -1047,12 +1090,26 @@ extern "C"
 
   /**
    * @brief Signature of a function to free dynamic memory.
+   * @ingroup Callbacks
    **/
   typedef void (*OrthancPluginFree) (void* buffer);
 
 
 
   /**
+   * @brief Signature of a function to set the content of a node
+   * encoding a binary DICOM tag, into a JSON or XML document
+   * generated for DICOMweb.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginDicomWebSetBinaryNode) (
+    OrthancPluginDicomWebNode*       node,
+    OrthancPluginDicomWebBinaryMode  mode,
+    const char*                      bulkDataUri);
+    
+
+
+  /**
    * @brief Callback for writing to the storage area.
    *
    * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
@@ -1422,7 +1479,7 @@ extern "C"
 
 
   /**
-   * @brief Callback executed to unserialized a custom job.
+   * @brief Callback executed to unserialize a custom job.
    * 
    * Signature of a callback function that unserializes a job that was
    * saved in the Orthanc database.
@@ -1440,6 +1497,60 @@ extern "C"
 
 
   /**
+   * @brief Callback executed to update the metrics of the plugin.
+   * 
+   * Signature of a callback function that is called by Orthanc
+   * whenever a monitoring tool (such as Prometheus) asks the current
+   * values of the metrics. This callback gives the plugin a chance to
+   * update its metrics, by calling OrthancPluginSetMetricsValue().
+   * This is typically useful for metrics that are expensive to
+   * acquire.
+   * 
+   * @see OrthancPluginRegisterRefreshMetrics()
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginRefreshMetricsCallback) ();
+
+  
+
+  /**
+   * @brief Callback executed to encode a binary tag in DICOMweb.
+   * 
+   * Signature of a callback function that is called by Orthanc
+   * whenever a DICOM tag that contains a binary value must be written
+   * to a JSON or XML node, while a DICOMweb document is being
+   * generated. The value representation (VR) of the DICOM tag can be
+   * OB, OD, OF, OL, OW, or UN.
+   * 
+   * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml()
+   * @param node The node being generated, as provided by Orthanc.
+   * @param setter The setter to be used to encode the content of the node. If
+   * the setter is not called, the binary tag is not written to the output document.
+   * @param levelDepth The depth of the node in the DICOM hierarchy of sequences.
+   * This parameter gives the number of elements in the "levelTagGroup", 
+   * "levelTagElement", and "levelIndex" arrays.
+   * @param levelTagGroup The group of the parent DICOM tags in the hierarchy.
+   * @param levelTagElement The element of the parent DICOM tags in the hierarchy.
+   * @param levelIndex The index of the node in the parent sequences of the hiearchy.
+   * @param tagGroup The group of the DICOM tag of interest.
+   * @param tagElement The element of the DICOM tag of interest.
+   * @param vr The value representation of the binary DICOM node.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginDicomWebBinaryCallback) (
+    OrthancPluginDicomWebNode*          node,
+    OrthancPluginDicomWebSetBinaryNode  setter,
+    uint32_t                            levelDepth,
+    const uint16_t*                     levelTagGroup,
+    const uint16_t*                     levelTagElement,
+    const uint32_t*                     levelIndex,
+    uint16_t                            tagGroup,
+    uint16_t                            tagElement,
+    OrthancPluginValueRepresentation    vr);
+
+
+
+  /**
    * @brief Data structure that contains information about the Orthanc core.
    **/
   typedef struct _OrthancPluginContext_t
@@ -1531,7 +1642,9 @@ extern "C"
         sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
         sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
         sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
-        sizeof(int32_t) != sizeof(OrthancPluginConstraintType))
+        sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode))
     {
       /* Mismatch in the size of the enumerations */
       return 0;
@@ -6526,6 +6639,160 @@ extern "C"
   }
 
 
+
+  typedef struct
+  {
+    const char*               name;
+    float                     value;
+    OrthancPluginMetricsType  type;
+  } _OrthancPluginSetMetricsValue;
+
+  /**
+   * @brief Set the value of a metrics.
+   *
+   * This function sets the value of a metrics to monitor the behavior
+   * of the plugin through tools such as Prometheus. The values of all
+   * the metrics are stored within the Orthanc context.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param name The name of the metrics to be set.
+   * @param value The value of the metrics.
+   * @param type The type of the metrics. This parameter is only taken into consideration
+   * the first time this metrics is set.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsValue(
+    OrthancPluginContext*     context,
+    const char*               name,
+    float                     value,
+    OrthancPluginMetricsType  type)
+  {
+    _OrthancPluginSetMetricsValue params;
+    params.name = name;
+    params.value = value;
+    params.type = type;
+    context->InvokeService(context, _OrthancPluginService_SetMetricsValue, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRefreshMetricsCallback  callback;
+  } _OrthancPluginRegisterRefreshMetricsCallback;
+
+  /**
+   * @brief Register a callback to refresh the metrics.
+   *
+   * This function registers a callback to refresh the metrics. The
+   * callback must make calls to OrthancPluginSetMetricsValue().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback function to handle the refresh.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRefreshMetricsCallback(
+    OrthancPluginContext*               context,
+    OrthancPluginRefreshMetricsCallback callback)
+  {
+    _OrthancPluginRegisterRefreshMetricsCallback params;
+    params.callback = callback;
+    context->InvokeService(context, _OrthancPluginService_RegisterRefreshMetricsCallback, &params);
+  }
+
+
+
+
+  typedef struct
+  {
+    char**                               target;
+    const void*                          dicom;
+    uint32_t                             dicomSize;
+    OrthancPluginDicomWebBinaryCallback  callback;
+  } _OrthancPluginEncodeDicomWeb;
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb JSON.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb JSON representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @see OrthancPluginCreateDicom()
+   * @return The NULL value in case of error, or the JSON document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson(
+    OrthancPluginContext*                context,
+    const void*                          dicom,
+    uint32_t                             dicomSize,
+    OrthancPluginDicomWebBinaryCallback  callback)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb XML.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb XML representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @return The NULL value in case of error, or the JSON document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @see OrthancPluginCreateDicom()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml(
+    OrthancPluginContext*                context,
+    const void*                          dicom,
+    uint32_t                             dicomSize,
+    OrthancPluginDicomWebBinaryCallback  callback)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+  
+
 #ifdef  __cplusplus
 }
 #endif
diff -pruN 1.5.3+dfsg-1/Plugins/Samples/Basic/Plugin.c 1.5.4+dfsg-1/Plugins/Samples/Basic/Plugin.c
--- 1.5.3+dfsg-1/Plugins/Samples/Basic/Plugin.c	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Plugins/Samples/Basic/Plugin.c	2019-02-08 14:02:50.000000000 +0000
@@ -38,8 +38,11 @@ ORTHANC_PLUGINS_API OrthancPluginErrorCo
 
   if (request->method != OrthancPluginHttpMethod_Get)
   {
-    // NB: Calling "OrthancPluginSendMethodNotAllowed(context, output, "GET");"
-    // is preferable. This is a sample to demonstrate "OrthancPluginSetHttpErrorDetails()".
+    /**
+     * NB: Calling "OrthancPluginSendMethodNotAllowed(context, output,
+     * "GET");" is preferable. This is a sample to demonstrate
+     * "OrthancPluginSetHttpErrorDetails()". 
+     **/
     OrthancPluginSetHttpErrorDetails(context, output, "This Callback1() can only be used by a GET call", 1 /* log */);
     return OrthancPluginErrorCode_ParameterOutOfRange;
   }
@@ -263,6 +266,21 @@ ORTHANC_PLUGINS_API OrthancPluginErrorCo
 }
 
 
+ORTHANC_PLUGINS_API void DicomWebBinaryCallback(
+  OrthancPluginDicomWebNode*          node,
+  OrthancPluginDicomWebSetBinaryNode  setter,
+  uint32_t                            levelDepth,
+  const uint16_t*                     levelTagGroup,
+  const uint16_t*                     levelTagElement,
+  const uint32_t*                     levelIndex,
+  uint16_t                            tagGroup,
+  uint16_t                            tagElement,
+  OrthancPluginValueRepresentation    vr)
+{
+  setter(node, OrthancPluginDicomWebBinaryMode_BulkDataUri, "HelloURI");
+}
+
+
 ORTHANC_PLUGINS_API OrthancPluginErrorCode OnStoredCallback(OrthancPluginDicomInstance* instance,
                                                             const char* instanceId)
 {
@@ -286,9 +304,7 @@ ORTHANC_PLUGINS_API OrthancPluginErrorCo
   json = OrthancPluginGetInstanceSimplifiedJson(context, instance);
   if (first)
   {
-    /* Only print the first DICOM instance */
     printf("[%s]\n", json);
-    first = 0;
   }
   OrthancPluginFreeString(context, json);
 
@@ -301,6 +317,18 @@ ORTHANC_PLUGINS_API OrthancPluginErrorCo
     OrthancPluginLogError(context, "Instance has no reception date, should never happen!");
   }
 
+  json = OrthancPluginEncodeDicomWebXml(context,
+                                         OrthancPluginGetInstanceData(context, instance),
+                                         OrthancPluginGetInstanceSize(context, instance),
+                                         DicomWebBinaryCallback);
+  if (first)
+  {
+    printf("[%s]\n", json);
+    first = 0;    /* Only print the first DICOM instance */
+  }
+  OrthancPluginFreeString(context, json);
+  
+
   return OrthancPluginErrorCode_Success;
 }
 
@@ -335,6 +363,8 @@ ORTHANC_PLUGINS_API OrthancPluginErrorCo
 
     case OrthancPluginChangeType_OrthancStarted:
     {
+      OrthancPluginSetMetricsValue(context, "sample_started", 1, OrthancPluginMetricsType_Default); 
+
       /* Make REST requests to the built-in Orthanc API */
       OrthancPluginRestApiGet(context, &tmp, "/changes");
       OrthancPluginFreeMemoryBuffer(context, &tmp);
@@ -392,6 +422,13 @@ ORTHANC_PLUGINS_API int32_t FilterIncomi
 }
 
 
+ORTHANC_PLUGINS_API void RefreshMetrics()
+{
+  static unsigned int count = 0;
+  OrthancPluginSetMetricsValue(context, "sample_counter", count++, OrthancPluginMetricsType_Default); 
+}
+
+
 ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
 {
   char info[1024], *s;
@@ -455,7 +492,9 @@ ORTHANC_PLUGINS_API int32_t OrthancPlugi
   OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback);
   OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
   OrthancPluginRegisterIncomingHttpRequestFilter(context, FilterIncomingHttpRequest);
+  OrthancPluginRegisterRefreshMetricsCallback(context, RefreshMetrics);
 
+  
   /* Declare several properties of the plugin */
   OrthancPluginSetRootUri(context, "/plugin/hello");
   OrthancPluginSetDescription(context, "This is the description of the sample plugin that can be seen in Orthanc Explorer.");
diff -pruN 1.5.3+dfsg-1/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp 1.5.4+dfsg-1/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
--- 1.5.3+dfsg-1/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -2033,4 +2033,21 @@ namespace OrthancPlugins
     }
   }
 #endif
+
+
+#if HAS_ORTHANC_PLUGIN_METRICS == 1
+  MetricsTimer::MetricsTimer(const char* name) :
+    name_(name)
+  {
+    start_ = boost::posix_time::microsec_clock::universal_time();
+  }
+  
+  MetricsTimer::~MetricsTimer()
+  {
+    const boost::posix_time::ptime stop = boost::posix_time::microsec_clock::universal_time();
+    const boost::posix_time::time_duration diff = stop - start_;
+    OrthancPluginSetMetricsValue(GetGlobalContext(), name_.c_str(), diff.total_milliseconds(),
+                                 OrthancPluginMetricsType_Timer);
+  }
+#endif
 }
diff -pruN 1.5.3+dfsg-1/Plugins/Samples/Common/OrthancPluginCppWrapper.h 1.5.4+dfsg-1/Plugins/Samples/Common/OrthancPluginCppWrapper.h
--- 1.5.3+dfsg-1/Plugins/Samples/Common/OrthancPluginCppWrapper.h	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Plugins/Samples/Common/OrthancPluginCppWrapper.h	2019-02-08 14:02:50.000000000 +0000
@@ -38,6 +38,7 @@
 #include <orthanc/OrthancCPlugin.h>
 #include <boost/noncopyable.hpp>
 #include <boost/lexical_cast.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
 #include <json/value.h>
 #include <vector>
 #include <list>
@@ -49,10 +50,10 @@
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
 #define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision)        \
   (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major ||                      \
-  (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major &&                    \
-  (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor ||                    \
-  (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor &&                  \
-  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision))))
+   (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major &&                    \
+    (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor ||                    \
+     (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor &&                  \
+      ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision))))
 #endif
 
 
@@ -78,6 +79,12 @@
 #  define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS  0
 #endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 4)
+#  define HAS_ORTHANC_PLUGIN_METRICS  1
+#else
+#  define HAS_ORTHANC_PLUGIN_METRICS  0
+#endif
+
 
 
 namespace OrthancPlugins
@@ -740,4 +747,26 @@ namespace OrthancPlugins
                               int priority);
   };
 #endif
+
+
+#if HAS_ORTHANC_PLUGIN_METRICS == 1
+  inline void SetMetricsValue(char* name,
+                              float value)
+  {
+    OrthancPluginSetMetricsValue(GetGlobalContext(), name,
+                                 value, OrthancPluginMetricsType_Default);
+  }
+
+  class MetricsTimer : public boost::noncopyable
+  {
+  private:
+    std::string               name_;
+    boost::posix_time::ptime  start_;
+
+  public:
+    MetricsTimer(const char* name);
+
+    ~MetricsTimer();
+  };
+#endif
 }
diff -pruN 1.5.3+dfsg-1/Resources/CMake/CivetwebConfiguration.cmake 1.5.4+dfsg-1/Resources/CMake/CivetwebConfiguration.cmake
--- 1.5.3+dfsg-1/Resources/CMake/CivetwebConfiguration.cmake	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Resources/CMake/CivetwebConfiguration.cmake	2019-02-08 14:02:50.000000000 +0000
@@ -47,6 +47,10 @@ if (STATIC_BUILD OR NOT USE_SYSTEM_CIVET
 
   source_group(ThirdParty\\Civetweb REGULAR_EXPRESSION ${CIVETWEB_SOURCES_DIR}/.*)
 
+  add_definitions(
+    -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1
+    )
+
 else()
   CHECK_INCLUDE_FILE_CXX(civetweb.h HAVE_CIVETWEB_H)
   if (NOT HAVE_CIVETWEB_H)
@@ -61,4 +65,8 @@ else()
   endif()
 
   link_libraries(civetweb)
+
+  add_definitions(
+    -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=0
+    )
 endif()
diff -pruN 1.5.3+dfsg-1/Resources/CMake/OrthancFrameworkConfiguration.cmake 1.5.4+dfsg-1/Resources/CMake/OrthancFrameworkConfiguration.cmake
--- 1.5.3+dfsg-1/Resources/CMake/OrthancFrameworkConfiguration.cmake	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Resources/CMake/OrthancFrameworkConfiguration.cmake	2019-02-08 14:02:50.000000000 +0000
@@ -448,6 +448,7 @@ if (ENABLE_DCMTK)
 
   set(ORTHANC_DICOM_SOURCES_INTERNAL
     ${ORTHANC_ROOT}/Core/DicomParsing/DicomModification.cpp
+    ${ORTHANC_ROOT}/Core/DicomParsing/DicomWebJsonVisitor.cpp
     ${ORTHANC_ROOT}/Core/DicomParsing/FromDcmtkBridge.cpp
     ${ORTHANC_ROOT}/Core/DicomParsing/ParsedDicomFile.cpp
     ${ORTHANC_ROOT}/Core/DicomParsing/ToDcmtkBridge.cpp
@@ -532,6 +533,7 @@ else()
   list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
     ${ORTHANC_ROOT}/Core/Cache/SharedArchive.cpp
     ${ORTHANC_ROOT}/Core/FileStorage/FilesystemStorage.cpp
+    ${ORTHANC_ROOT}/Core/MetricsRegistry.cpp
     ${ORTHANC_ROOT}/Core/MultiThreading/RunnableWorkersPool.cpp
     ${ORTHANC_ROOT}/Core/MultiThreading/Semaphore.cpp
     ${ORTHANC_ROOT}/Core/MultiThreading/SharedMessageQueue.cpp
diff -pruN 1.5.3+dfsg-1/Resources/CMake/OrthancFrameworkParameters.cmake 1.5.4+dfsg-1/Resources/CMake/OrthancFrameworkParameters.cmake
--- 1.5.3+dfsg-1/Resources/CMake/OrthancFrameworkParameters.cmake	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Resources/CMake/OrthancFrameworkParameters.cmake	2019-02-08 14:02:50.000000000 +0000
@@ -3,7 +3,7 @@
 #####################################################################
 
 # Version of the build, should always be "mainline" except in release branches
-set(ORTHANC_VERSION "1.5.3")
+set(ORTHANC_VERSION "1.5.4")
 
 # Version of the database schema. History:
 #   * Orthanc 0.1.0 -> Orthanc 0.3.0 = no versioning
@@ -17,7 +17,7 @@ set(ORTHANC_DATABASE_VERSION 6)
 # 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.3")
+set(ORTHANC_API_VERSION "1.4")
 
 
 #####################################################################
diff -pruN 1.5.3+dfsg-1/Resources/CMake/VisualStudioPrecompiledHeaders.cmake 1.5.4+dfsg-1/Resources/CMake/VisualStudioPrecompiledHeaders.cmake
--- 1.5.3+dfsg-1/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	2019-02-08 14:02:50.000000000 +0000
@@ -1,6 +1,6 @@
 macro(ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS PrecompiledHeaders PrecompiledSource Sources Target)
   get_filename_component(PrecompiledBasename ${PrecompiledHeaders} NAME_WE)
-  set(PrecompiledBinary "${PrecompiledBasename}_$(ConfigurationName).pch")
+  set(PrecompiledBinary "${PrecompiledBasename}_${CMAKE_BUILD_TYPE}_${CMAKE_GENERATOR_PLATFORM}.pch")
 
   set_source_files_properties(${PrecompiledSource}
     PROPERTIES COMPILE_FLAGS "/Yc\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\""
diff -pruN 1.5.3+dfsg-1/Resources/Configuration.json 1.5.4+dfsg-1/Resources/Configuration.json
--- 1.5.3+dfsg-1/Resources/Configuration.json	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Resources/Configuration.json	2019-02-08 14:02:50.000000000 +0000
@@ -17,6 +17,18 @@
   // a RAM-drive or a SSD device for performance reasons.
   "IndexDirectory" : "OrthancStorage",
 
+  // Path to the directory where Orthanc stores its large temporary
+  // files. The content of this folder can be safely deleted if
+  // Orthanc once stopped. The folder must exist. The corresponding
+  // filesystem must be properly sized, given that for instance a ZIP
+  // archive of DICOM images created by a job can weight several GBs,
+  // and that there might be up to "min(JobsHistorySize,
+  // MediaArchiveSize)" archives to be stored simultaneously. If not
+  // set, Orthanc will use the default temporary folder of the
+  // operating system (such as "/tmp/" on UNIX-like systems, or
+  // "C:/Temp" on Microsoft Windows).
+  // "TemporaryDirectory" : "/tmp/Orthanc/",
+
   // Enable the transparent compression of the DICOM instances
   "StorageCompression" : false,
 
@@ -363,6 +375,9 @@
   // caveats: https://eklitzke.org/the-caveats-of-tcp-nodelay
   "TcpNoDelay" : true,
 
+  // Number of threads that are used by the embedded HTTP server.
+  "HttpThreadsCount" : 50,
+
   // 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
   // this option might prevent the upgrade to newer versions of Orthanc.
@@ -476,5 +491,11 @@
   // answers, but not to filter the DICOM resources (balance between
   // the two modes). By default, the mode is "Always", which
   // corresponds to the behavior of Orthanc <= 1.5.0.
-  "StorageAccessOnFind" : "Always"
+  "StorageAccessOnFind" : "Always",
+
+  // Whether Orthanc monitors its metrics (new in Orthanc 1.5.4). If
+  // set to "true", the metrics can be retrieved at
+  // "/tools/metrics-prometheus" formetted using the Prometheus
+  // text-based exposition format.
+  "MetricsEnabled" : true
 }
diff -pruN 1.5.3+dfsg-1/Resources/DownloadOrthancFramework.cmake 1.5.4+dfsg-1/Resources/DownloadOrthancFramework.cmake
--- 1.5.3+dfsg-1/Resources/DownloadOrthancFramework.cmake	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/Resources/DownloadOrthancFramework.cmake	2019-02-08 14:02:50.000000000 +0000
@@ -97,6 +97,8 @@ if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "h
         set(ORTHANC_FRAMEWORK_MD5 "099671538865e5da96208b37494d6718")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.2")
         set(ORTHANC_FRAMEWORK_MD5 "8867050f3e9a1ce6157c1ea7a9433b1b")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.3")
+        set(ORTHANC_FRAMEWORK_MD5 "bf2f5ed1adb8b0fc5f10d278e68e1dfe")
       endif()
     endif()
   endif()
diff -pruN 1.5.3+dfsg-1/Resources/Samples/Python/DeleteAllStudies.py 1.5.4+dfsg-1/Resources/Samples/Python/DeleteAllStudies.py
--- 1.5.3+dfsg-1/Resources/Samples/Python/DeleteAllStudies.py	1970-01-01 00:00:00.000000000 +0000
+++ 1.5.4+dfsg-1/Resources/Samples/Python/DeleteAllStudies.py	2019-02-08 14:02:50.000000000 +0000
@@ -0,0 +1,42 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# 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.
+# 
+# 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/>.
+
+
+
+import os
+import os.path
+import sys
+import RestToolbox
+
+def PrintHelp():
+    print('Delete all the imaging studies that are stored in Orthanc\n')
+    print('Usage: %s <URL>\n' % sys.argv[0])
+    print('Example: %s http://127.0.0.1:8042/\n' % sys.argv[0])
+    exit(-1)
+
+if len(sys.argv) != 2:
+    PrintHelp()
+
+URL = sys.argv[1]
+
+for study in RestToolbox.DoGet('%s/studies' % URL):
+    print('Removing study: %s' % study)
+    RestToolbox.DoDelete('%s/studies/%s' % (URL, study))
diff -pruN 1.5.3+dfsg-1/UnitTestsSources/DicomMapTests.cpp 1.5.4+dfsg-1/UnitTestsSources/DicomMapTests.cpp
--- 1.5.3+dfsg-1/UnitTestsSources/DicomMapTests.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/UnitTestsSources/DicomMapTests.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -37,12 +37,15 @@
 #include "../Core/OrthancException.h"
 #include "../Core/DicomFormat/DicomMap.h"
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/ToDcmtkBridge.h"
 #include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/DicomParsing/DicomWebJsonVisitor.h"
 
 #include "../OrthancServer/DicomInstanceToStore.h"
 
 #include <memory>
 #include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcvrat.h>
 
 using namespace Orthanc;
 
@@ -551,3 +554,298 @@ TEST(DicomMap, ExtractMainDicomTags)
   ASSERT_EQ("F", b.GetValue(DICOM_TAG_SLICE_THICKNESS).GetContent());
   ASSERT_FALSE(b.HasOnlyMainDicomTags());
 }
+
+
+
+TEST(DicomWebJson, Multiplicity)
+{
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.4.html
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "SB1^SB2^SB3^SB4^SB5");
+  dicom.ReplacePlainString(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "1\\2.3\\4");
+  dicom.ReplacePlainString(DICOM_TAG_IMAGE_POSITION_PATIENT, "");
+
+  Orthanc::DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+
+  {
+    const Json::Value& tag = visitor.GetResult() ["00200037"];
+    const Json::Value& value = tag["Value"];
+  
+    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
+    ASSERT_EQ(2u, tag.getMemberNames().size());
+    ASSERT_EQ(3u, value.size());
+    ASSERT_EQ(Json::realValue, value[1].type());
+    ASSERT_FLOAT_EQ(1.0f, value[0].asFloat());
+    ASSERT_FLOAT_EQ(2.3f, value[1].asFloat());
+    ASSERT_FLOAT_EQ(4.0f, value[2].asFloat());
+  }
+
+  {
+    const Json::Value& tag = visitor.GetResult() ["00200032"];
+    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
+    ASSERT_EQ(1u, tag.getMemberNames().size());
+  }
+
+  std::string xml;
+  visitor.FormatXml(xml);
+}
+
+
+TEST(DicomWebJson, NullValue)
+{
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.5.html
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "1.5\\\\\\2.5");
+
+  Orthanc::DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+
+  {
+    const Json::Value& tag = visitor.GetResult() ["00200037"];
+    const Json::Value& value = tag["Value"];
+  
+    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
+    ASSERT_EQ(2u, tag.getMemberNames().size());
+    ASSERT_EQ(4u, value.size());
+    ASSERT_EQ(Json::realValue, value[0].type());
+    ASSERT_EQ(Json::nullValue, value[1].type());
+    ASSERT_EQ(Json::nullValue, value[2].type());
+    ASSERT_EQ(Json::realValue, value[3].type());
+    ASSERT_FLOAT_EQ(1.5f, value[0].asFloat());
+    ASSERT_FLOAT_EQ(2.5f, value[3].asFloat());
+  }
+
+  std::string xml;
+  visitor.FormatXml(xml);
+}
+
+
+static void SetTagKey(ParsedDicomFile& dicom,
+                      const DicomTag& tag,
+                      const DicomTag& value)
+{
+  // This function emulates a call to function
+  // "dicom.GetDcmtkObject().getDataset()->putAndInsertTagKey(tag,
+  // value)" that was not available in DCMTK 3.6.0
+
+  std::auto_ptr<DcmAttributeTag> element(new DcmAttributeTag(ToDcmtkBridge::Convert(tag)));
+
+  DcmTagKey v = ToDcmtkBridge::Convert(value);
+  if (!element->putTagVal(v).good())
+  {
+    throw OrthancException(ErrorCode_InternalError);
+  }
+
+  dicom.GetDcmtkObject().getDataset()->insert(element.release());
+}
+
+
+TEST(DicomWebJson, ValueRepresentation)
+{
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DicomTag(0x0040, 0x0241), "AE");
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x1010), "AS");
+  SetTagKey(dicom, DicomTag(0x0020, 0x9165), DicomTag(0x0010, 0x0020));
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0052), "CS");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0012), "DA");
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x1020), "42");  // DS
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x002a), "DT");
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x9431), "43");  // FL
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x1163), "44");  // FD
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x1160), "45");  // IS
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0070), "LO");
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x4000), "LT");
+  dicom.ReplacePlainString(DicomTag(0x0028, 0x2000), "OB");
+  dicom.ReplacePlainString(DicomTag(0x7fe0, 0x0009), "OD");
+  dicom.ReplacePlainString(DicomTag(0x0064, 0x0009), "OF");
+  dicom.ReplacePlainString(DicomTag(0x0066, 0x0040), "46");
+  ASSERT_THROW(dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "O"), OrthancException);
+  dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "OWOW");
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x0010), "PN");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0050), "SH");
+  dicom.ReplacePlainString(DicomTag(0x0018, 0x6020), "-15");  // SL
+  dicom.ReplacePlainString(DicomTag(0x0018, 0x9219), "-16");  // SS
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0081), "ST");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0013), "TM");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0119), "UC");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0016), "UI");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x1161), "128");  // UL
+  dicom.ReplacePlainString(DicomTag(0x4342, 0x1234), "UN");   // Inexistent tag
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0120), "UR");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0301), "17");   // US
+  dicom.ReplacePlainString(DicomTag(0x0040, 0x0031), "UT");  
+  
+  Orthanc::DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+
+  std::string s;
+  
+  ASSERT_EQ("AE", visitor.GetResult() ["00400241"]["vr"].asString());
+  ASSERT_EQ("AE", visitor.GetResult() ["00400241"]["Value"][0].asString());
+  ASSERT_EQ("AS", visitor.GetResult() ["00101010"]["vr"].asString());
+  ASSERT_EQ("AS", visitor.GetResult() ["00101010"]["Value"][0].asString());
+  ASSERT_EQ("AT", visitor.GetResult() ["00209165"]["vr"].asString());
+  ASSERT_EQ("00100020", visitor.GetResult() ["00209165"]["Value"][0].asString());
+  ASSERT_EQ("CS", visitor.GetResult() ["00080052"]["vr"].asString());
+  ASSERT_EQ("CS", visitor.GetResult() ["00080052"]["Value"][0].asString());
+  ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["vr"].asString());
+  ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["Value"][0].asString());
+  ASSERT_EQ("DS", visitor.GetResult() ["00101020"]["vr"].asString());
+  ASSERT_FLOAT_EQ(42.0f, visitor.GetResult() ["00101020"]["Value"][0].asFloat());
+  ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["vr"].asString());
+  ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["Value"][0].asString());
+  ASSERT_EQ("FL", visitor.GetResult() ["00109431"]["vr"].asString());
+  ASSERT_FLOAT_EQ(43.0f, visitor.GetResult() ["00109431"]["Value"][0].asFloat());
+  ASSERT_EQ("FD", visitor.GetResult() ["00081163"]["vr"].asString());
+  ASSERT_FLOAT_EQ(44.0f, visitor.GetResult() ["00081163"]["Value"][0].asFloat());
+  ASSERT_EQ("IS", visitor.GetResult() ["00081160"]["vr"].asString());
+  ASSERT_FLOAT_EQ(45.0f, visitor.GetResult() ["00081160"]["Value"][0].asFloat());
+  ASSERT_EQ("LO", visitor.GetResult() ["00080070"]["vr"].asString());
+  ASSERT_EQ("LO", visitor.GetResult() ["00080070"]["Value"][0].asString());
+  ASSERT_EQ("LT", visitor.GetResult() ["00104000"]["vr"].asString());
+  ASSERT_EQ("LT", visitor.GetResult() ["00104000"]["Value"][0].asString());
+
+  ASSERT_EQ("OB", visitor.GetResult() ["00282000"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00282000"]["InlineBinary"].asString());
+  ASSERT_EQ("OB", s);
+
+#if DCMTK_VERSION_NUMBER >= 361
+  ASSERT_EQ("OD", visitor.GetResult() ["7FE00009"]["vr"].asString());
+#else
+  ASSERT_EQ("UN", visitor.GetResult() ["7FE00009"]["vr"].asString());
+#endif
+
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["7FE00009"]["InlineBinary"].asString());
+  ASSERT_EQ("OD", s);
+
+  ASSERT_EQ("OF", visitor.GetResult() ["00640009"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00640009"]["InlineBinary"].asString());
+  ASSERT_EQ("OF", s);
+
+#if DCMTK_VERSION_NUMBER < 361
+  ASSERT_EQ("UN", visitor.GetResult() ["00660040"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00660040"]["InlineBinary"].asString());
+  ASSERT_EQ("46", s);
+#elif DCMTK_VERSION_NUMBER == 361
+  ASSERT_EQ("UL", visitor.GetResult() ["00660040"]["vr"].asString());
+  ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt());
+#elif DCMTK_VERSION_NUMBER > 361
+  ASSERT_EQ("OL", visitor.GetResult() ["00660040"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00660040"]["InlineBinary"].asString());
+  ASSERT_EQ("46", s);
+#endif
+
+  ASSERT_EQ("OW", visitor.GetResult() ["00281201"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00281201"]["InlineBinary"].asString());
+  ASSERT_EQ("OWOW", s);
+
+  ASSERT_EQ("PN", visitor.GetResult() ["00100010"]["vr"].asString());
+  ASSERT_EQ("PN", visitor.GetResult() ["00100010"]["Value"][0]["Alphabetic"].asString());
+
+  ASSERT_EQ("SH", visitor.GetResult() ["00080050"]["vr"].asString());
+  ASSERT_EQ("SH", visitor.GetResult() ["00080050"]["Value"][0].asString());
+
+  ASSERT_EQ("SL", visitor.GetResult() ["00186020"]["vr"].asString());
+  ASSERT_EQ(-15, visitor.GetResult() ["00186020"]["Value"][0].asInt());
+
+  ASSERT_EQ("SS", visitor.GetResult() ["00189219"]["vr"].asString());
+  ASSERT_EQ(-16, visitor.GetResult() ["00189219"]["Value"][0].asInt());
+
+  ASSERT_EQ("ST", visitor.GetResult() ["00080081"]["vr"].asString());
+  ASSERT_EQ("ST", visitor.GetResult() ["00080081"]["Value"][0].asString());
+
+  ASSERT_EQ("TM", visitor.GetResult() ["00080013"]["vr"].asString());
+  ASSERT_EQ("TM", visitor.GetResult() ["00080013"]["Value"][0].asString());
+
+#if DCMTK_VERSION_NUMBER >= 361
+  ASSERT_EQ("UC", visitor.GetResult() ["00080119"]["vr"].asString());
+  ASSERT_EQ("UC", visitor.GetResult() ["00080119"]["Value"][0].asString());
+#else
+  ASSERT_EQ("UN", visitor.GetResult() ["00080119"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00080119"]["InlineBinary"].asString());
+  ASSERT_EQ("UC", s);
+#endif
+
+  ASSERT_EQ("UI", visitor.GetResult() ["00080016"]["vr"].asString());
+  ASSERT_EQ("UI", visitor.GetResult() ["00080016"]["Value"][0].asString());
+
+  ASSERT_EQ("UL", visitor.GetResult() ["00081161"]["vr"].asString());
+  ASSERT_EQ(128u, visitor.GetResult() ["00081161"]["Value"][0].asUInt());
+
+  ASSERT_EQ("UN", visitor.GetResult() ["43421234"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["43421234"]["InlineBinary"].asString());
+  ASSERT_EQ("UN", s);
+
+#if DCMTK_VERSION_NUMBER >= 361
+  ASSERT_EQ("UR", visitor.GetResult() ["00080120"]["vr"].asString());
+  ASSERT_EQ("UR", visitor.GetResult() ["00080120"]["Value"][0].asString());
+#else
+  ASSERT_EQ("UN", visitor.GetResult() ["00080120"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00080120"]["InlineBinary"].asString());
+  ASSERT_EQ("UR", s);
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+  ASSERT_EQ("US", visitor.GetResult() ["00080301"]["vr"].asString());
+  ASSERT_EQ(17u, visitor.GetResult() ["00080301"]["Value"][0].asUInt());
+#else
+  ASSERT_EQ("UN", visitor.GetResult() ["00080301"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00080301"]["InlineBinary"].asString());
+  ASSERT_EQ("17", s);
+#endif
+
+  ASSERT_EQ("UT", visitor.GetResult() ["00400031"]["vr"].asString());
+  ASSERT_EQ("UT", visitor.GetResult() ["00400031"]["Value"][0].asString());
+
+  std::string xml;
+  visitor.FormatXml(xml);
+}
+
+
+TEST(DicomWebJson, Sequence)
+{
+  ParsedDicomFile dicom(false);
+  
+  {
+    std::auto_ptr<DcmSequenceOfItems> sequence(new DcmSequenceOfItems(DCM_ReferencedSeriesSequence));
+
+    for (unsigned int i = 0; i < 3; i++)
+    {
+      std::auto_ptr<DcmItem> item(new DcmItem);
+      std::string s = "item" + boost::lexical_cast<std::string>(i);
+      item->putAndInsertString(DCM_ReferencedSOPInstanceUID, s.c_str(), OFFalse);
+      ASSERT_TRUE(sequence->insert(item.release(), false, false).good());
+    }
+
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->insert(sequence.release(), false, false).good());
+  }
+
+  Orthanc::DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+
+  ASSERT_EQ("SQ", visitor.GetResult() ["00081115"]["vr"].asString());
+  ASSERT_EQ(3u, visitor.GetResult() ["00081115"]["Value"].size());
+
+  std::set<std::string> items;
+  
+  for (Json::Value::ArrayIndex i = 0; i < 3; i++)
+  {
+    ASSERT_EQ(1u, visitor.GetResult() ["00081115"]["Value"][i].size());
+    ASSERT_EQ(1u, visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["Value"].size());
+    ASSERT_EQ("UI", visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["vr"].asString());
+    items.insert(visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["Value"][0].asString());
+  }
+
+  ASSERT_EQ(3u, items.size());
+  ASSERT_TRUE(items.find("item0") != items.end());
+  ASSERT_TRUE(items.find("item1") != items.end());
+  ASSERT_TRUE(items.find("item2") != items.end());
+
+  std::string xml;
+  visitor.FormatXml(xml);
+}
diff -pruN 1.5.3+dfsg-1/UnitTestsSources/ImageTests.cpp 1.5.4+dfsg-1/UnitTestsSources/ImageTests.cpp
--- 1.5.3+dfsg-1/UnitTestsSources/ImageTests.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/UnitTestsSources/ImageTests.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -185,7 +185,7 @@ TEST(PngWriter, EndToEnd)
 
   {
     Orthanc::TemporaryFile tmp;
-    Orthanc::SystemToolbox::WriteFile(s, tmp.GetPath());
+    tmp.Write(s);
 
     Orthanc::PngReader r2;
     r2.ReadFromFile(tmp.GetPath());
@@ -411,7 +411,7 @@ TEST(PamWriter, EndToEnd)
 
   {
     Orthanc::TemporaryFile tmp;
-    Orthanc::SystemToolbox::WriteFile(s, tmp.GetPath());
+    tmp.Write(s);
 
     Orthanc::PamReader r2;
     r2.ReadFromFile(tmp.GetPath());
@@ -433,4 +433,3 @@ TEST(PamWriter, EndToEnd)
     }
   }
 }
-
diff -pruN 1.5.3+dfsg-1/UnitTestsSources/ServerIndexTests.cpp 1.5.4+dfsg-1/UnitTestsSources/ServerIndexTests.cpp
--- 1.5.3+dfsg-1/UnitTestsSources/ServerIndexTests.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/UnitTestsSources/ServerIndexTests.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -293,8 +293,8 @@ TEST_F(DatabaseWrapperTest, Simple)
     ASSERT_EQ("e", l.front());
   }
 
-  std::list<MetadataType> md;
-  index_->ListAvailableMetadata(md, a[4]);
+  std::map<MetadataType, std::string> md;
+  index_->GetAllMetadata(md, a[4]);
   ASSERT_EQ(0u, md.size());
 
   index_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", 
@@ -303,11 +303,11 @@ TEST_F(DatabaseWrapperTest, Simple)
   index_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"));
   index_->SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE");
   
-  index_->ListAvailableMetadata(md, a[4]);
+  index_->GetAllMetadata(md, a[4]);
   ASSERT_EQ(1u, md.size());
-  ASSERT_EQ(MetadataType_Instance_RemoteAet, md.front());
+  ASSERT_EQ("PINNACLE", md[MetadataType_Instance_RemoteAet]);
   index_->SetMetadata(a[4], MetadataType_ModifiedFrom, "TUTU");
-  index_->ListAvailableMetadata(md, a[4]);
+  index_->GetAllMetadata(md, a[4]);
   ASSERT_EQ(2u, md.size());
 
   std::map<MetadataType, std::string> md2;
@@ -317,9 +317,9 @@ TEST_F(DatabaseWrapperTest, Simple)
   ASSERT_EQ("PINNACLE", md2[MetadataType_Instance_RemoteAet]);
 
   index_->DeleteMetadata(a[4], MetadataType_ModifiedFrom);
-  index_->ListAvailableMetadata(md, a[4]);
+  index_->GetAllMetadata(md, a[4]);
   ASSERT_EQ(1u, md.size());
-  ASSERT_EQ(MetadataType_Instance_RemoteAet, md.front());
+  ASSERT_EQ("PINNACLE", md[MetadataType_Instance_RemoteAet]);
 
   index_->GetAllMetadata(md2, a[4]);
   ASSERT_EQ(1u, md2.size());
@@ -702,10 +702,12 @@ TEST(ServerIndex, AttachmentRecycling)
 
   index.SetMaximumStorageSize(10);
 
-  Json::Value tmp;
-  index.ComputeStatistics(tmp);
-  ASSERT_EQ(0, tmp["CountPatients"].asInt());
-  ASSERT_EQ(0, boost::lexical_cast<int>(tmp["TotalDiskSize"].asString()));
+  uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
+  index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                            countStudies, countSeries, countInstances);
+
+  ASSERT_EQ(0u, countPatients);
+  ASSERT_EQ(0u, diskSize);
 
   ServerIndex::Attachments attachments;
 
@@ -747,17 +749,19 @@ TEST(ServerIndex, AttachmentRecycling)
     ASSERT_EQ(hasher.HashInstance(), toStore.GetHasher().HashInstance());
   }
 
-  index.ComputeStatistics(tmp);
-  ASSERT_EQ(10, tmp["CountPatients"].asInt());
-  ASSERT_EQ(0, boost::lexical_cast<int>(tmp["TotalDiskSize"].asString()));
+  index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                            countStudies, countSeries, countInstances);
+  ASSERT_EQ(10u, countPatients);
+  ASSERT_EQ(0u, diskSize);
 
   for (size_t i = 0; i < ids.size(); i++)
   {
     FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5");
     index.AddAttachment(info, ids[i]);
 
-    index.ComputeStatistics(tmp);
-    ASSERT_GE(10, boost::lexical_cast<int>(tmp["TotalDiskSize"].asString()));
+    index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                              countStudies, countSeries, countInstances);
+    ASSERT_GE(10u, diskSize);
   }
 
   // Because the DB is in memory, the SQLite index must not have been created
@@ -800,10 +804,12 @@ TEST(ServerIndex, Overwrite)
     std::string id = hasher.HashInstance();
     context.GetIndex().SetOverwriteInstances(overwrite);
 
-    Json::Value tmp;
-    context.GetIndex().ComputeStatistics(tmp);
-    ASSERT_EQ(0, tmp["CountInstances"].asInt());
-    ASSERT_EQ(0, boost::lexical_cast<int>(tmp["TotalDiskSize"].asString()));
+    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
+    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                                           countStudies, countSeries, countInstances);
+
+    ASSERT_EQ(0u, countInstances);
+    ASSERT_EQ(0u, diskSize);
 
     {
       DicomInstanceToStore toStore;
@@ -820,13 +826,13 @@ TEST(ServerIndex, Overwrite)
     ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, id, FileContentType_Dicom));
     ASSERT_TRUE(context.GetIndex().LookupAttachment(json1, id, FileContentType_DicomAsJson));
 
-    context.GetIndex().ComputeStatistics(tmp);
-    ASSERT_EQ(1, tmp["CountInstances"].asInt());
-    ASSERT_EQ(dicom1.GetCompressedSize() + json1.GetCompressedSize(),
-              boost::lexical_cast<size_t>(tmp["TotalDiskSize"].asString()));
-    ASSERT_EQ(dicom1.GetUncompressedSize() + json1.GetUncompressedSize(),
-              boost::lexical_cast<size_t>(tmp["TotalUncompressedSize"].asString()));
+    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                                           countStudies, countSeries, countInstances);
+    ASSERT_EQ(1u, countInstances);
+    ASSERT_EQ(dicom1.GetCompressedSize() + json1.GetCompressedSize(), diskSize);
+    ASSERT_EQ(dicom1.GetUncompressedSize() + json1.GetUncompressedSize(), uncompressedSize);
 
+    Json::Value tmp;
     context.ReadDicomAsJson(tmp, id);
     ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString());
     
@@ -855,12 +861,11 @@ TEST(ServerIndex, Overwrite)
     ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, id, FileContentType_Dicom));
     ASSERT_TRUE(context.GetIndex().LookupAttachment(json2, id, FileContentType_DicomAsJson));
 
-    context.GetIndex().ComputeStatistics(tmp);
-    ASSERT_EQ(1, tmp["CountInstances"].asInt());
-    ASSERT_EQ(dicom2.GetCompressedSize() + json2.GetCompressedSize(),
-              boost::lexical_cast<size_t>(tmp["TotalDiskSize"].asString()));
-    ASSERT_EQ(dicom2.GetUncompressedSize() + json2.GetUncompressedSize(),
-              boost::lexical_cast<size_t>(tmp["TotalUncompressedSize"].asString()));
+    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                                           countStudies, countSeries, countInstances);
+    ASSERT_EQ(1u, countInstances);
+    ASSERT_EQ(dicom2.GetCompressedSize() + json2.GetCompressedSize(), diskSize);
+    ASSERT_EQ(dicom2.GetUncompressedSize() + json2.GetUncompressedSize(), uncompressedSize);
 
     if (overwrite)
     {
diff -pruN 1.5.3+dfsg-1/UnitTestsSources/UnitTestsMain.cpp 1.5.4+dfsg-1/UnitTestsSources/UnitTestsMain.cpp
--- 1.5.3+dfsg-1/UnitTestsSources/UnitTestsMain.cpp	2019-01-25 14:41:17.000000000 +0000
+++ 1.5.4+dfsg-1/UnitTestsSources/UnitTestsMain.cpp	2019-02-08 14:02:50.000000000 +0000
@@ -41,6 +41,7 @@
 #include "../Core/DicomFormat/DicomTag.h"
 #include "../Core/HttpServer/HttpToolbox.h"
 #include "../Core/Logging.h"
+#include "../Core/MetricsRegistry.h"
 #include "../Core/OrthancException.h"
 #include "../Core/TemporaryFile.h"
 #include "../Core/Toolbox.h"
@@ -755,6 +756,8 @@ TEST(Toolbox, Enumerations)
   ASSERT_EQ(MimeType_Xml, StringToMimeType("application/xml"));
   ASSERT_EQ(MimeType_Xml, StringToMimeType("text/xml"));
   ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml)));
+  ASSERT_EQ(MimeType_DicomWebJson, StringToMimeType(EnumerationToString(MimeType_DicomWebJson)));
+  ASSERT_EQ(MimeType_DicomWebXml, StringToMimeType(EnumerationToString(MimeType_DicomWebXml)));
   ASSERT_THROW(StringToMimeType("nope"), OrthancException);
 
   ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Patient));
@@ -1236,6 +1239,120 @@ TEST(Toolbox, SubstituteVariables)
 }
 
 
+TEST(MetricsRegistry, Basic)
+{
+  {
+    MetricsRegistry m;
+    m.SetEnabled(false);
+    m.SetValue("hello.world", 42.5f);
+    
+    std::string s;
+    m.ExportPrometheusText(s);
+    ASSERT_TRUE(s.empty());
+  }
+
+  {
+    MetricsRegistry m;
+    m.Register("hello.world", MetricsType_Default);
+    
+    std::string s;
+    m.ExportPrometheusText(s);
+    ASSERT_TRUE(s.empty());
+  }
+
+  {
+    MetricsRegistry m;
+    m.SetValue("hello.world", 42.5f);
+    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.world"));
+    ASSERT_THROW(m.GetMetricsType("nope"), OrthancException);
+    
+    std::string s;
+    m.ExportPrometheusText(s);
+
+    std::vector<std::string> t;
+    Toolbox::TokenizeString(t, s, '\n');
+    ASSERT_EQ(2u, t.size());
+    ASSERT_EQ("hello.world 42.5 ", t[0].substr(0, 17));
+    ASSERT_TRUE(t[1].empty());
+  }
+
+  {
+    MetricsRegistry m;
+    m.Register("hello.max", MetricsType_MaxOver10Seconds);
+    m.SetValue("hello.max", 10);
+    m.SetValue("hello.max", 20);
+    m.SetValue("hello.max", -10);
+    m.SetValue("hello.max", 5);
+
+    m.Register("hello.min", MetricsType_MinOver10Seconds);
+    m.SetValue("hello.min", 10);
+    m.SetValue("hello.min", 20);
+    m.SetValue("hello.min", -10);
+    m.SetValue("hello.min", 5);
+    
+    m.Register("hello.default", MetricsType_Default);
+    m.SetValue("hello.default", 10);
+    m.SetValue("hello.default", 20);
+    m.SetValue("hello.default", -10);
+    m.SetValue("hello.default", 5);
+    
+    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("hello.max"));
+    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("hello.min"));
+    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.default"));
+
+    std::string s;
+    m.ExportPrometheusText(s);
+
+    std::vector<std::string> t;
+    Toolbox::TokenizeString(t, s, '\n');
+    ASSERT_EQ(4u, t.size());
+    ASSERT_TRUE(t[3].empty());
+
+    std::map<std::string, std::string> u;
+    for (size_t i = 0; i < t.size() - 1; i++)
+    {
+      std::vector<std::string> v;
+      Toolbox::TokenizeString(v, t[i], ' ');
+      u[v[0]] = v[1];
+    }
+
+    ASSERT_EQ("20", u["hello.max"]);
+    ASSERT_EQ("-10", u["hello.min"]);
+    ASSERT_EQ("5", u["hello.default"]);
+  }
+
+  {
+    MetricsRegistry m;
+
+    m.SetValue("a", 10);
+    m.SetValue("b", 10, MetricsType_MinOver10Seconds);
+
+    m.Register("c", MetricsType_MaxOver10Seconds);
+    m.SetValue("c", 10, MetricsType_MinOver10Seconds);
+
+    m.Register("d", MetricsType_MaxOver10Seconds);
+    m.Register("d", MetricsType_Default);
+
+    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("a"));
+    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b"));
+    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("c"));
+    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("d"));
+  }
+
+  {
+    MetricsRegistry m;
+
+    {
+      MetricsRegistry::Timer t1(m, "a");
+      MetricsRegistry::Timer t2(m, "b", MetricsType_MinOver10Seconds);
+    }
+
+    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("a"));
+    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b"));
+  }
+}
+
+
 int main(int argc, char **argv)
 {
   Logging::Initialize();
