changeset 2981:eff50153a7b3 db-changes

integration mainline->db-changes
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 06 Dec 2018 15:58:08 +0100
parents 3fabf9a673f6 (current diff) 63b724c7b046 (diff)
children d547f998c947
files
diffstat 142 files changed, 5222 insertions(+), 2819 deletions(-) [+]
line wrap: on
line diff
--- a/AUTHORS	Thu Oct 18 10:48:11 2018 +0200
+++ b/AUTHORS	Thu Dec 06 15:58:08 2018 +0100
@@ -14,7 +14,8 @@
   4000 Liege
   Belgium
 
-* Osimis S.A. <info@osimis.io>
+* Osimis S.A.
   Rue du Bois Saint-Jean 15/1
   4102 Seraing
   Belgium
+  http://www.osimis.io/
--- a/CMakeLists.txt	Thu Oct 18 10:48:11 2018 +0200
+++ b/CMakeLists.txt	Thu Dec 06 15:58:08 2018 +0100
@@ -58,6 +58,7 @@
   OrthancServer/DicomInstanceToStore.cpp
   OrthancServer/ExportedResource.cpp
   OrthancServer/LuaScripting.cpp
+  OrthancServer/OrthancConfiguration.cpp
   OrthancServer/OrthancFindRequestHandler.cpp
   OrthancServer/OrthancHttpHandler.cpp
   OrthancServer/OrthancInitialization.cpp
--- a/Core/Cache/SharedArchive.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Cache/SharedArchive.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -47,6 +47,8 @@
     {
       delete it->second;
       archive_.erase(it);
+
+      lru_.Invalidate(id);
     }
   }
 
@@ -59,7 +61,7 @@
 
     if (it == that.archive_.end())
     {
-      throw OrthancException(ErrorCode_InexistentItem);
+      item_ = NULL;
     }
     else
     {
@@ -69,6 +71,20 @@
   }
 
 
+  IDynamicObject& SharedArchive::Accessor::GetItem() const
+  {
+    if (item_ == NULL)
+    {
+      // "IsValid()" should have been called
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *item_;
+    }
+  }  
+
+
   SharedArchive::SharedArchive(size_t maxSize) : 
     maxSize_(maxSize)
   {
@@ -96,12 +112,12 @@
     if (archive_.size() == maxSize_)
     {
       // The quota has been reached, remove the oldest element
-      std::string oldest = lru_.RemoveOldest();
-      RemoveInternal(oldest);
+      RemoveInternal(lru_.GetOldest());
     }
 
     std::string id = Toolbox::GenerateUuid();
     RemoveInternal(id);  // Should never be useful because of UUID
+
     archive_[id] = obj;
     lru_.Add(id);
 
@@ -113,7 +129,6 @@
   {
     boost::mutex::scoped_lock lock(mutex_);
     RemoveInternal(id);      
-    lru_.Invalidate(id);
   }
 
 
@@ -121,14 +136,14 @@
   {
     items.clear();
 
-    boost::mutex::scoped_lock lock(mutex_);
+    {
+      boost::mutex::scoped_lock lock(mutex_);
 
-    for (Archive::const_iterator it = archive_.begin();
-         it != archive_.end(); ++it)
-    {
-      items.push_back(it->first);
+      for (Archive::const_iterator it = archive_.begin();
+           it != archive_.end(); ++it)
+      {
+        items.push_back(it->first);
+      }
     }
   }
 }
-
-
--- a/Core/Cache/SharedArchive.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Cache/SharedArchive.h	Thu Dec 06 15:58:08 2018 +0100
@@ -72,10 +72,12 @@
       Accessor(SharedArchive& that,
                const std::string& id);
 
-      IDynamicObject& GetItem() const
+      bool IsValid() const
       {
-        return *item_;
-      }      
+        return item_ != NULL;
+      }
+      
+      IDynamicObject& GetItem() const;
     };
 
 
--- a/Core/Compression/DeflateBaseCompressor.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Compression/DeflateBaseCompressor.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -45,8 +45,8 @@
   {
     if (level >= 10)
     {
-      LOG(ERROR) << "Zlib compression level must be between 0 (no compression) and 9 (highest compression)";
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Zlib compression level must be between 0 (no compression) and 9 (highest compression)");
     }
 
     compressionLevel_ = level;
@@ -63,8 +63,7 @@
 
     if (compressedSize < sizeof(uint64_t))
     {
-      LOG(ERROR) << "The compressed buffer is ill-formed";
-      throw OrthancException(ErrorCode_CorruptedFile);
+      throw OrthancException(ErrorCode_CorruptedFile, "The compressed buffer is ill-formed");
     }
 
     uint64_t size;
--- a/Core/Compression/GzipCompressor.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Compression/GzipCompressor.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -271,8 +271,8 @@
       // The uncompressed size was not that properly guess, presumably
       // because of a file size over 4GB. Should fallback to
       // stream-based decompression.
-      LOG(ERROR) << "The uncompressed size of a gzip-encoded buffer was not properly guessed";
-      throw OrthancException(ErrorCode_NotImplemented);
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "The uncompressed size of a gzip-encoded buffer was not properly guessed");
     }
   }
 }
--- a/Core/Compression/ZipWriter.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Compression/ZipWriter.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -124,8 +124,8 @@
 
     if (path_.size() == 0)
     {
-      LOG(ERROR) << "Please call SetOutputPath() before creating the file";
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "Please call SetOutputPath() before creating the file");
     }
 
     hasFileInZip_ = false;
@@ -168,8 +168,8 @@
   {
     if (level >= 10)
     {
-      LOG(ERROR) << "ZIP compression level must be between 0 (no compression) and 9 (highest compression)";
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "ZIP compression level must be between 0 (no compression) and 9 (highest compression)");
     }
 
     Close();
@@ -228,8 +228,7 @@
   {
     if (!hasFileInZip_)
     {
-      LOG(ERROR) << "Call first OpenFile()";
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Call first OpenFile()");
     }
 
     const size_t maxBytesInAStep = std::numeric_limits<int32_t>::max();
--- a/Core/Compression/ZlibCompressor.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Compression/ZlibCompressor.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -117,8 +117,8 @@
 
     if (!HasPrefixWithUncompressedSize())
     {
-      LOG(ERROR) << "Cannot guess the uncompressed size of a zlib-encoded buffer";
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot guess the uncompressed size of a zlib-encoded buffer");
     }
 
     uint64_t uncompressedSize = ReadUncompressedSizePrefix(compressed, compressedSize);
--- a/Core/DicomFormat/DicomInstanceHasher.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomFormat/DicomInstanceHasher.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -53,7 +53,7 @@
         seriesUid_.size() == 0 ||
         instanceUid_.size() == 0)
     {
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat, "missing StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID");
     }
   }
 
--- a/Core/DicomFormat/DicomTag.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomFormat/DicomTag.h	Thu Dec 06 15:58:08 2018 +0100
@@ -175,6 +175,17 @@
   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_CONTRAST_BOLUS_AGENT(0x0018, 0x0010);
+  static const DicomTag DICOM_TAG_STUDY_ID(0x0020, 0x0010);
+  static const DicomTag DICOM_TAG_SERIES_NUMBER(0x0020, 0x0011);
+  static const DicomTag DICOM_TAG_PATIENT_SEX(0x0010, 0x0040);
+  static const DicomTag DICOM_TAG_LATERALITY(0x0020, 0x0060);
+  static const DicomTag DICOM_TAG_BODY_PART_EXAMINED(0x0018, 0x0015);
+  static const DicomTag DICOM_TAG_VIEW_POSITION(0x0018, 0x5101);
+  static const DicomTag DICOM_TAG_MANUFACTURER(0x0008, 0x0070);
+  static const DicomTag DICOM_TAG_PATIENT_ORIENTATION(0x0020, 0x0020);
+  static const DicomTag DICOM_TAG_PATIENT_COMMENTS(0x0010, 0x4000);
+  static const DicomTag DICOM_TAG_PATIENT_SPECIES_DESCRIPTION(0x0010, 0x2201);
+  static const DicomTag DICOM_TAG_STUDY_COMMENTS(0x0032, 0x4000);
 
   // Tags used within the Stone of Orthanc
   static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009);
--- a/Core/DicomFormat/DicomValue.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomFormat/DicomValue.h	Thu Dec 06 15:58:08 2018 +0100
@@ -33,8 +33,9 @@
 
 #pragma once
 
+#include "../Enumerations.h"
+
 #include <stdint.h>
-#include <string>
 #include <boost/noncopyable.hpp>
 #include <json/value.h>
 
@@ -92,7 +93,7 @@
 
     void FormatDataUriScheme(std::string& target) const
     {
-      FormatDataUriScheme(target, "application/octet-stream");
+      FormatDataUriScheme(target, MIME_BINARY);
     }
 #endif
 
--- a/Core/DicomNetworking/DicomServer.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomNetworking/DicomServer.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -316,8 +316,8 @@
   {
     if (modalities_ == NULL)
     {
-      LOG(ERROR) << "No list of modalities was provided to the DICOM server";
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No list of modalities was provided to the DICOM server");
     }
     
     Stop();
@@ -327,8 +327,8 @@
       (NET_ACCEPTOR, OFstatic_cast(int, port_), /*opt_acse_timeout*/ 30, &pimpl_->network_);
     if (cond.bad())
     {
-      LOG(ERROR) << "cannot create network: " << cond.text();
-      throw OrthancException(ErrorCode_DicomPortInUse);
+      throw OrthancException(ErrorCode_DicomPortInUse,
+                             "cannot create network: " + std::string(cond.text()));
     }
 
     continue_ = true;
--- a/Core/DicomNetworking/DicomUserConnection.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomNetworking/DicomUserConnection.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -164,8 +164,8 @@
   {
     if (cond.bad())
     {
-      LOG(ERROR) << "DicomUserConnection: " << std::string(cond.text());
-       throw OrthancException(ErrorCode_NetworkProtocol);
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "DicomUserConnection: " + std::string(cond.text()));
     }
   }
 
@@ -173,8 +173,8 @@
   {
     if (!IsOpen())
     {
-      LOG(ERROR) << "DicomUserConnection: First open the connection";
-      throw OrthancException(ErrorCode_NetworkProtocol);
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "DicomUserConnection: First open the connection");
     }
   }
 
--- a/Core/DicomNetworking/Internals/FindScp.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomNetworking/Internals/FindScp.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -200,9 +200,9 @@
           assert(data.modalities_ != NULL);
           if (!data.modalities_->LookupAETitle(modality, *data.remoteAet_))
           {
-            LOG(ERROR) << "Modality with AET \"" << *data.remoteAet_
-                       << "\" is not defined in the \"DicomModalities\" configuration option";
-            throw OrthancException(ErrorCode_UnknownModality);
+            throw OrthancException(ErrorCode_UnknownModality,
+                                   "Modality with AET \"" + (*data.remoteAet_) +
+                                   "\" is not defined in the \"DicomModalities\" configuration option");
           }
 
           
--- a/Core/DicomNetworking/RemoteModalityParameters.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomNetworking/RemoteModalityParameters.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -87,8 +87,9 @@
     if (value <= 0 || 
         value >= 65535)
     {
-      LOG(ERROR) << "A TCP port number must be in range [1..65534], but found: " << value;
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "A TCP port number must be in range [1..65534], but found: " +
+                             boost::lexical_cast<std::string>(value));
     }
   }
 
--- a/Core/DicomParsing/DicomModification.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomParsing/DicomModification.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -256,8 +256,8 @@
       {
         if (!identifierGenerator_->Apply(mapped, original, level, currentSource_))
         {
-          LOG(ERROR) << "Unable to generate an anonymized ID";
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Unable to generate an anonymized ID");
         }
       }
 
@@ -873,28 +873,28 @@
     // Sanity checks at the patient level
     if (level_ == ResourceType_Patient && !IsReplaced(DICOM_TAG_PATIENT_ID))
     {
-      LOG(ERROR) << "When modifying a patient, her PatientID is required to be modified";
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying a patient, her PatientID is required to be modified");
     }
 
     if (!allowManualIdentifiers_)
     {
       if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
       {
-        LOG(ERROR) << "When modifying a patient, the StudyInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, the StudyInstanceUID cannot be manually modified");
       }
 
       if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
       {
-        LOG(ERROR) << "When modifying a patient, the SeriesInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, the SeriesInstanceUID cannot be manually modified");
       }
 
       if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
       {
-        LOG(ERROR) << "When modifying a patient, the SopInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, the SopInstanceUID cannot be manually modified");
       }
     }
 
@@ -902,22 +902,22 @@
     // Sanity checks at the study level
     if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_PATIENT_ID))
     {
-      LOG(ERROR) << "When modifying a study, the parent PatientID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying a study, the parent PatientID cannot be manually modified");
     }
 
     if (!allowManualIdentifiers_)
     {
       if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
       {
-        LOG(ERROR) << "When modifying a study, the SeriesInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a study, the SeriesInstanceUID cannot be manually modified");
       }
 
       if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
       {
-        LOG(ERROR) << "When modifying a study, the SopInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a study, the SopInstanceUID cannot be manually modified");
       }
     }
 
@@ -925,22 +925,22 @@
     // Sanity checks at the series level
     if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_PATIENT_ID))
     {
-      LOG(ERROR) << "When modifying a series, the parent PatientID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying a series, the parent PatientID cannot be manually modified");
     }
 
     if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
     {
-      LOG(ERROR) << "When modifying a series, the parent StudyInstanceUID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying a series, the parent StudyInstanceUID cannot be manually modified");
     }
 
     if (!allowManualIdentifiers_)
     {
       if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
       {
-        LOG(ERROR) << "When modifying a series, the SopInstanceUID cannot be manually modified";
-        throw OrthancException(ErrorCode_BadRequest);
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a series, the SopInstanceUID cannot be manually modified");
       }
     }
 
@@ -948,20 +948,20 @@
     // Sanity checks at the instance level
     if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_PATIENT_ID))
     {
-      LOG(ERROR) << "When modifying an instance, the parent PatientID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying an instance, the parent PatientID cannot be manually modified");
     }
 
     if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
     {
-      LOG(ERROR) << "When modifying an instance, the parent StudyInstanceUID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying an instance, the parent StudyInstanceUID cannot be manually modified");
     }
 
     if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
     {
-      LOG(ERROR) << "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified";
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified");
     }
 
 
@@ -1082,10 +1082,10 @@
 
       if (!force && IsDatabaseKey(tag))
       {
-        LOG(ERROR) << "Marking tag \"" << name << "\" as to be "
-                   << (operation == DicomModification::TagOperation_Keep ? "kept" : "removed")
-                   << " requires the \"Force\" option to be set to true";
-        throw OrthancException(ErrorCode_BadRequest);
+        throw OrthancException(ErrorCode_BadRequest,
+                               "Marking tag \"" + name + "\" as to be " +
+                               (operation == DicomModification::TagOperation_Keep ? "kept" : "removed") +
+                               " requires the \"Force\" option to be set to true");
       }
 
       switch (operation)
@@ -1126,9 +1126,9 @@
 
       if (!force && IsDatabaseKey(tag))
       {
-        LOG(ERROR) << "Marking tag \"" << name << "\" as to be replaced "
-                   << "requires the \"Force\" option to be set to true";
-        throw OrthancException(ErrorCode_BadRequest);
+        throw OrthancException(ErrorCode_BadRequest,
+                               "Marking tag \"" + name + "\" as to be replaced " +
+                               "requires the \"Force\" option to be set to true");
       }
 
       target.Replace(tag, value, false);
@@ -1153,8 +1153,8 @@
     }
     else
     {
-      LOG(ERROR) << "Member \"" << member << "\" should be a Boolean value";
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Member \"" + member + "\" should be a Boolean value");
     }
   }
 
@@ -1271,8 +1271,8 @@
   {
     if (identifierGenerator_ != NULL)
     {
-      LOG(ERROR) << "Cannot serialize a DicomModification with a custom identifier generator";
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot serialize a DicomModification with a custom identifier generator");
     }
 
     value = Json::objectValue;
--- a/Core/DicomParsing/FromDcmtkBridge.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomParsing/FromDcmtkBridge.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -119,16 +119,16 @@
 
     if (!dictionary.loadDictionary(tmp.GetPath().c_str()))
     {
-      LOG(ERROR) << "Cannot read embedded dictionary. Under Windows, make sure that " 
-                 << "your TEMP directory does not contain special characters.";
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot read embedded dictionary. Under Windows, make sure that " 
+                             "your TEMP directory does not contain special characters.");
     }
 #else
     if (!dictionary.loadFromMemory(content))
     {
-      LOG(ERROR) << "Cannot read embedded dictionary. Under Windows, make sure that " 
-                 << "your TEMP directory does not contain special characters.";
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot read embedded dictionary. Under Windows, make sure that " 
+                             "your TEMP directory does not contain special characters.");
     }
 #endif
   }
@@ -289,8 +289,9 @@
     /* make sure data dictionary is loaded */
     if (!dcmDataDict.isDictionaryLoaded())
     {
-      LOG(ERROR) << "No DICOM dictionary loaded, check environment variable: " << DCM_DICT_ENVIRONMENT_VARIABLE;
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "No DICOM dictionary loaded, check environment variable: " +
+                             std::string(DCM_DICT_ENVIRONMENT_VARIABLE));
     }
 
     {
@@ -298,8 +299,8 @@
       DcmTag key(0x0010, 0x1030); // This is PatientWeight
       if (key.getEVR() != EVR_DS)
       {
-        LOG(ERROR) << "The DICOM dictionary has not been correctly read";
-        throw OrthancException(ErrorCode_InternalError);
+        throw OrthancException(ErrorCode_InternalError,
+                               "The DICOM dictionary has not been correctly read");
       }
     }
   }
@@ -370,8 +371,7 @@
         char buf[128];
         sprintf(buf, "Trying to register private tag (%04x,%04x), but it must have an odd group >= 0x0009",
                 tag.GetGroup(), tag.GetElement());
-        LOG(ERROR) << buf;
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancException(ErrorCode_ParameterOutOfRange, std::string(buf));
       }
 
       entry.reset(new DcmDictEntry(tag.GetGroup(),
@@ -392,8 +392,8 @@
 
       if (locker->findEntry(name.c_str()))
       {
-        LOG(ERROR) << "Cannot register two tags with the same symbolic name \"" << name << "\"";
-        throw OrthancException(ErrorCode_AlreadyExistingTag);
+        throw OrthancException(ErrorCode_AlreadyExistingTag,
+                               "Cannot register two tags with the same symbolic name \"" + name + "\"");
       }
 
       locker->addEntry(entry.release());
@@ -1511,7 +1511,7 @@
     const std::string* decoded = &utf8Value;
 
     if (decodeDataUriScheme &&
-        boost::starts_with(utf8Value, "data:application/octet-stream;base64,"))
+        boost::starts_with(utf8Value, URI_SCHEME_PREFIX_BINARY))
     {
       std::string mime;
       if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value))
@@ -1673,9 +1673,9 @@
 
     if (!ok)
     {
-      LOG(ERROR) << "While creating a DICOM instance, tag (" << tag.Format()
-                 << ") has out-of-range value: \"" << *decoded << "\"";
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "While creating a DICOM instance, tag (" + tag.Format() +
+                             ") has out-of-range value: \"" + (*decoded) + "\"");
     }
   }
 
@@ -1800,8 +1800,9 @@
             (value.asString().length() != 0 &&
              !GetDicomEncoding(encoding, value.asCString())))
         {
-          LOG(ERROR) << "Unknown encoding while creating DICOM from JSON: " << value;
-          throw OrthancException(ErrorCode_BadRequest);
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Unknown encoding while creating DICOM from JSON: " +
+                                 value.toStyledString());
         }
 
         if (value.asString().length() == 0)
@@ -1924,8 +1925,9 @@
     result->transferInit();
     if (!result->read(is).good())
     {
-      LOG(ERROR) << "Cannot parse an invalid DICOM file (size: " << size << " bytes)";
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot parse an invalid DICOM file (size: " +
+                             boost::lexical_cast<std::string>(size) + " bytes)");
     }
 
     result->loadAllDataIntoMemory();
@@ -2044,8 +2046,8 @@
 
     if (output.type() != Json::objectValue)
     {
-      LOG(ERROR) << "Lua: IncomingFindRequestFilter must return a table";
-      throw OrthancException(ErrorCode_LuaBadOutput);
+      throw OrthancException(ErrorCode_LuaBadOutput,
+                             "Lua: IncomingFindRequestFilter must return a table");
     }
 
     Json::Value::Members members = output.getMemberNames();
@@ -2054,8 +2056,9 @@
     {
       if (output[members[i]].type() != Json::stringValue)
       {
-        LOG(ERROR) << "Lua: IncomingFindRequestFilter must return a table mapping names of DICOM tags to strings";
-        throw OrthancException(ErrorCode_LuaBadOutput);
+        throw OrthancException(ErrorCode_LuaBadOutput,
+                               "Lua: IncomingFindRequestFilter must return a table "
+                               "mapping names of DICOM tags to strings");
       }
 
       DicomTag tag(ParseTag(members[i]));
@@ -2186,8 +2189,8 @@
           std::string s = Toolbox::ConvertFromUtf8(newValue, encoding);
           if (element.putString(s.c_str()) != EC_Normal)
           {
-            LOG(ERROR) << "Cannot replace value of tag: " << tag.Format();
-            throw OrthancException(ErrorCode_InternalError);
+            throw OrthancException(ErrorCode_InternalError,
+                                   "Cannot replace value of tag: " + tag.Format());
           }
 
           break;
--- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -633,8 +633,8 @@
                              &dataset, frame, startFragment, &uncompressed[0],
                              uncompressed.size(), decompressedColorModel).good())
       {
-        LOG(ERROR) << "Cannot decode a palette image";
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Cannot decode a palette image");
       }
 
       return DecodeLookupTable(target, info, dataset,
@@ -648,8 +648,8 @@
                              &dataset, frame, startFragment, target->GetBuffer(), 
                              target->GetSize(), decompressedColorModel).good())
       {
-        LOG(ERROR) << "Cannot decode a non-palette image";
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Cannot decode a non-palette image");
       }
 
       return target.release();
@@ -806,8 +806,8 @@
       }
     }
 
-    LOG(ERROR) << "Cannot decode a DICOM image with the built-in decoder";
-    throw OrthancException(ErrorCode_BadFileFormat);
+    throw OrthancException(ErrorCode_BadFileFormat,
+                           "Cannot decode a DICOM image with the built-in decoder");
   }
 
 
--- a/Core/DicomParsing/ParsedDicomFile.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomParsing/ParsedDicomFile.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -82,8 +82,10 @@
 #include "ParsedDicomFile.h"
 
 #include "FromDcmtkBridge.h"
+#include "Internals/DicomFrameIndex.h"
 #include "ToDcmtkBridge.h"
-#include "Internals/DicomFrameIndex.h"
+
+#include "../Images/PamReader.h"
 #include "../Logging.h"
 #include "../OrthancException.h"
 #include "../Toolbox.h"
@@ -160,8 +162,6 @@
 
 
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
-  static const char* CONTENT_TYPE_OCTET_STREAM = "application/octet-stream";
-
   static void ParseTagAndGroup(DcmTagKey& key,
                                const std::string& tag)
   {
@@ -246,27 +246,28 @@
 
       virtual HttpCompression SetupHttpCompression(bool /*gzipAllowed*/,
                                                    bool /*deflateAllowed*/)
+        ORTHANC_OVERRIDE
       {
         // No support for compression
         return HttpCompression_None;
       }
 
-      virtual bool HasContentFilename(std::string& filename)
+      virtual bool HasContentFilename(std::string& filename) ORTHANC_OVERRIDE
       {
         return false;
       }
 
-      virtual std::string GetContentType()
+      virtual std::string GetContentType() ORTHANC_OVERRIDE
       {
-        return "";
+        return EnumerationToString(MimeType_Binary);
       }
 
-      virtual uint64_t  GetContentLength()
+      virtual uint64_t  GetContentLength() ORTHANC_OVERRIDE
       {
         return length_;
       }
  
-      virtual bool ReadNextChunk()
+      virtual bool ReadNextChunk() ORTHANC_OVERRIDE
       {
         assert(offset_ <= length_);
 
@@ -291,20 +292,21 @@
 
           if (!cond.good())
           {
-            LOG(ERROR) << "Error while sending a DICOM field: " << cond.text();
-            throw OrthancException(ErrorCode_InternalError);
+            throw OrthancException(ErrorCode_InternalError,
+                                   "Error while sending a DICOM field: " +
+                                   std::string(cond.text()));
           }
 
           return true;
         }
       }
  
-      virtual const char *GetChunkContent()
+      virtual const char *GetChunkContent() ORTHANC_OVERRIDE
       {
         return chunk_.c_str();
       }
  
-      virtual size_t GetChunkSize()
+      virtual size_t GetChunkSize() ORTHANC_OVERRIDE
       {
         return chunkSize_;
       }
@@ -362,14 +364,14 @@
             {
               if (pixelItem->getLength() == 0)
               {
-                output.AnswerBuffer(NULL, 0, CONTENT_TYPE_OCTET_STREAM);
+                output.AnswerBuffer(NULL, 0, MimeType_Binary);
                 return true;
               }
 
               Uint8* buffer = NULL;
               if (pixelItem->getUint8Array(buffer).good() && buffer)
               {
-                output.AnswerBuffer(buffer, pixelItem->getLength(), CONTENT_TYPE_OCTET_STREAM);
+                output.AnswerBuffer(buffer, pixelItem->getLength(), MimeType_Binary);
                 return true;
               }
             }
@@ -692,7 +694,7 @@
     const std::string* decoded = &utf8Value;
 
     if (decodeDataUriScheme &&
-        boost::starts_with(utf8Value, "data:application/octet-stream;base64,"))
+        boost::starts_with(utf8Value, URI_SCHEME_PREFIX_BINARY))
     {
       std::string mime;
       if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value))
@@ -825,7 +827,7 @@
     std::string serialized;
     if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset()))
     {
-      output.AnswerBuffer(serialized, CONTENT_TYPE_OCTET_STREAM);
+      output.AnswerBuffer(serialized, MimeType_Binary);
     }
   }
 #endif
@@ -954,8 +956,8 @@
     }
     else if (tmp->IsBinary())
     {
-      LOG(ERROR) << "Invalid binary string in the SpecificCharacterSet (0008,0005) tag";
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Invalid binary string in the SpecificCharacterSet (0008,0005) tag");
     }
     else if (tmp->IsNull() ||
              tmp->GetContent().empty())
@@ -972,9 +974,9 @@
       }
       else
       {
-        LOG(ERROR) << "Unsupported value for the SpecificCharacterSet (0008,0005) tag: \""
-                   << tmp->GetContent() << "\"";        
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "Unsupported value for the SpecificCharacterSet (0008,0005) tag: \"" +
+                               tmp->GetContent() + "\"");
       }
     }
 
@@ -1064,40 +1066,46 @@
 
   bool ParsedDicomFile::EmbedContentInternal(const std::string& dataUriScheme)
   {
-    std::string mime, content;
-    if (!Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme))
+    std::string mimeString, content;
+    if (!Toolbox::DecodeDataUriScheme(mimeString, content, dataUriScheme))
     {
       return false;
     }
 
-    Toolbox::ToLowerCase(mime);
+    Toolbox::ToLowerCase(mimeString);
+    MimeType mime = StringToMimeType(mimeString);
 
-    if (mime == "image/png")
+    switch (mime)
     {
+      case MimeType_Png:
 #if ORTHANC_ENABLE_PNG == 1
-      EmbedImage(mime, content);
+        EmbedImage(mime, content);
+        break;
 #else
-      LOG(ERROR) << "Orthanc was compiled without support of PNG";
-      throw OrthancException(ErrorCode_NotImplemented);
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Orthanc was compiled without support of PNG");
 #endif
-    }
-    else if (mime == "image/jpeg")
-    {
+
+      case MimeType_Jpeg:
 #if ORTHANC_ENABLE_JPEG == 1
-      EmbedImage(mime, content);
+        EmbedImage(mime, content);
+        break;
 #else
-      LOG(ERROR) << "Orthanc was compiled without support of JPEG";
-      throw OrthancException(ErrorCode_NotImplemented);
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Orthanc was compiled without support of JPEG");
 #endif
-    }
-    else if (mime == "application/pdf")
-    {
-      EmbedPdf(content);
-    }
-    else
-    {
-      LOG(ERROR) << "Unsupported MIME type for the content of a new DICOM file: " << mime;
-      throw OrthancException(ErrorCode_NotImplemented);
+
+      case MimeType_Pam:
+        EmbedImage(mime, content);
+        break;
+
+      case MimeType_Pdf:
+        EmbedPdf(content);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Unsupported MIME type for the content of a new DICOM file: " + mime);
     }
 
     return true;
@@ -1113,29 +1121,44 @@
   }
 
 
-#if (ORTHANC_ENABLE_JPEG == 1 &&                \
-     ORTHANC_ENABLE_PNG == 1)
-  void ParsedDicomFile::EmbedImage(const std::string& mime,
+  void ParsedDicomFile::EmbedImage(MimeType mime,
                                    const std::string& content)
   {
-    if (mime == "image/png")
-    {
-      PngReader reader;
-      reader.ReadFromMemory(content);
-      EmbedImage(reader);
-    }
-    else if (mime == "image/jpeg")
+    switch (mime)
     {
-      JpegReader reader;
-      reader.ReadFromMemory(content);
-      EmbedImage(reader);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
+    
+#if ORTHANC_ENABLE_JPEG == 1
+      case MimeType_Jpeg:
+      {
+        JpegReader reader;
+        reader.ReadFromMemory(content);
+        EmbedImage(reader);
+        break;
+      }
+#endif
+    
+#if ORTHANC_ENABLE_PNG == 1
+      case MimeType_Png:
+      {
+        PngReader reader;
+        reader.ReadFromMemory(content);
+        EmbedImage(reader);
+        break;
+      }
+#endif
+
+      case MimeType_Pam:
+      {
+        PamReader reader;
+        reader.ReadFromMemory(content);
+        EmbedImage(reader);
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
     }
   }
-#endif
 
 
   void ParsedDicomFile::EmbedImage(const ImageAccessor& accessor)
@@ -1162,7 +1185,9 @@
     ReplacePlainString(DICOM_TAG_COLUMNS, boost::lexical_cast<std::string>(accessor.GetWidth()));
     ReplacePlainString(DICOM_TAG_ROWS, boost::lexical_cast<std::string>(accessor.GetHeight()));
     ReplacePlainString(DICOM_TAG_SAMPLES_PER_PIXEL, "1");
-    ReplacePlainString(DICOM_TAG_NUMBER_OF_FRAMES, "1");
+
+    // The "Number of frames" must only be present in multi-frame images
+    //ReplacePlainString(DICOM_TAG_NUMBER_OF_FRAMES, "1");
 
     if (accessor.GetFormat() == PixelFormat_SignedGrayscale16)
     {
@@ -1173,14 +1198,14 @@
       ReplacePlainString(DICOM_TAG_PIXEL_REPRESENTATION, "0");  // Unsigned pixels
     }
 
-    ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0");  // Color channels are interleaved
-    SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2"); // by default, greyscale images are in MONOCHROME2
-
     unsigned int bytesPerPixel = 0;
 
     switch (accessor.GetFormat())
     {
       case PixelFormat_Grayscale8:
+        // By default, grayscale images are MONOCHROME2
+        SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
+
         ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "8");
         ReplacePlainString(DICOM_TAG_BITS_STORED, "8");
         ReplacePlainString(DICOM_TAG_HIGH_BIT, "7");
@@ -1195,10 +1220,18 @@
         ReplacePlainString(DICOM_TAG_BITS_STORED, "8");
         ReplacePlainString(DICOM_TAG_HIGH_BIT, "7");
         bytesPerPixel = 3;
+
+        // "Planar configuration" must only present if "Samples per
+        // Pixel" is greater than 1
+        ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0");  // Color channels are interleaved
+
         break;
 
       case PixelFormat_Grayscale16:
       case PixelFormat_SignedGrayscale16:
+        // By default, grayscale images are MONOCHROME2
+        SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
+
         ReplacePlainString(DICOM_TAG_BITS_ALLOCATED, "16");
         ReplacePlainString(DICOM_TAG_BITS_STORED, "16");
         ReplacePlainString(DICOM_TAG_HIGH_BIT, "15");
@@ -1337,8 +1370,7 @@
     if (pdf.size() < 5 ||  // (*)
         strncmp("%PDF-", pdf.c_str(), 5) != 0)
     {
-      LOG(ERROR) << "Not a PDF file";
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat, "Not a PDF file");
     }
 
     InvalidateCache();
@@ -1346,7 +1378,7 @@
     ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage);
     ReplacePlainString(FromDcmtkBridge::Convert(DCM_Modality), "OT");
     ReplacePlainString(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD");
-    ReplacePlainString(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), "application/pdf");
+    ReplacePlainString(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), MIME_PDF);
     //ReplacePlainString(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1");
 
     std::auto_ptr<DcmPolymorphOBOW> element(new DcmPolymorphOBOW(DCM_EncapsulatedDocument));
@@ -1388,7 +1420,7 @@
     if (!GetTagValue(sop, DICOM_TAG_SOP_CLASS_UID) ||
         !GetTagValue(mime, FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument)) ||
         sop != UID_EncapsulatedPDFStorage ||
-        mime != "application/pdf")
+        mime != MIME_PDF)
     {
       return false;
     }
@@ -1456,7 +1488,7 @@
 
 
   void ParsedDicomFile::GetRawFrame(std::string& target,
-                                    std::string& mime,
+                                    MimeType& mime,
                                     unsigned int frameId)
   {
     if (pimpl_->frameIndex_.get() == NULL)
@@ -1470,16 +1502,16 @@
     switch (transferSyntax)
     {
       case EXS_JPEGProcess1:
-        mime = "image/jpeg";
+        mime = MimeType_Jpeg;
         break;
        
       case EXS_JPEG2000LosslessOnly:
       case EXS_JPEG2000:
-        mime = "image/jp2";
+        mime = MimeType_Jpeg2000;
         break;
 
       default:
-        mime = "application/octet-stream";
+        mime = MimeType_Binary;
         break;
     }
   }
--- a/Core/DicomParsing/ParsedDicomFile.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/DicomParsing/ParsedDicomFile.h	Thu Dec 06 15:58:08 2018 +0100
@@ -183,11 +183,8 @@
 
     void EmbedImage(const ImageAccessor& accessor);
 
-#if (ORTHANC_ENABLE_JPEG == 1 &&  \
-     ORTHANC_ENABLE_PNG == 1)
-    void EmbedImage(const std::string& mime,
+    void EmbedImage(MimeType mime,
                     const std::string& content);
-#endif
 
     Encoding GetEncoding() const;
 
@@ -223,7 +220,7 @@
     bool ExtractPdf(std::string& pdf);
 
     void GetRawFrame(std::string& target, // OUT
-                     std::string& mime,   // OUT
+                     MimeType& mime,   // OUT
                      unsigned int frameId);  // IN
 
     unsigned int GetFramesCount() const;
--- a/Core/EnumerationDictionary.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/EnumerationDictionary.h	Thu Dec 06 15:58:08 2018 +0100
@@ -84,13 +84,10 @@
 
       Enumeration Translate(const std::string& str) const
       {
-        try
+        if (Toolbox::IsInteger(str))
         {
           return static_cast<Enumeration>(boost::lexical_cast<int>(str));
         }
-        catch (boost::bad_lexical_cast&)
-        {
-        }
 
         typename StringToEnumeration::const_iterator
           found = stringToEnumeration_.find(str);
--- a/Core/Enumerations.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Enumerations.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -44,6 +44,18 @@
 
 namespace Orthanc
 {
+  static const char* const MIME_CSS = "text/css";
+  static const char* const MIME_DICOM = "application/dicom";
+  static const char* const MIME_GIF = "image/gif";
+  static const char* const MIME_GZIP = "application/gzip";
+  static const char* const MIME_HTML = "text/html";
+  static const char* const MIME_JAVASCRIPT = "application/javascript";
+  static const char* const MIME_JPEG2000 = "image/jp2";
+  static const char* const MIME_PLAIN_TEXT = "text/plain";
+  static const char* const MIME_WEB_ASSEMBLY = "application/wasm";
+  static const char* const MIME_XML_2 = "text/xml";
+  static const char* const MIME_ZIP = "application/zip";
+
   // This function is autogenerated by the script
   // "Resources/GenerateErrorCodes.py"
   const char* EnumerationToString(ErrorCode error)
@@ -1019,6 +1031,67 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
+
+
+  const char* EnumerationToString(MimeType mime)
+  {
+    switch (mime)
+    {
+      case MimeType_Binary:
+        return MIME_BINARY;
+        
+      case MimeType_Dicom:
+        return MIME_DICOM;
+        
+      case MimeType_Jpeg:
+        return MIME_JPEG;
+        
+      case MimeType_Jpeg2000:
+        return MIME_JPEG2000;
+        
+      case MimeType_Json:
+        return MIME_JSON;
+        
+      case MimeType_Pdf:
+        return MIME_PDF;
+        
+      case MimeType_Png:
+        return MIME_PNG;
+        
+      case MimeType_Xml:
+        return MIME_XML;
+        
+      case MimeType_PlainText:
+        return MIME_PLAIN_TEXT;
+                
+      case MimeType_Pam:
+        return MIME_PAM;
+                
+      case MimeType_Html:
+        return MIME_HTML;
+                
+      case MimeType_Gzip:
+        return MIME_GZIP;
+                
+      case MimeType_JavaScript:
+        return MIME_JAVASCRIPT;
+                
+      case MimeType_Css:
+        return MIME_CSS;
+                
+      case MimeType_WebAssembly:
+        return MIME_WEB_ASSEMBLY;
+                
+      case MimeType_Gif:
+        return MIME_GIF;
+                
+      case MimeType_Zip:
+        return MIME_ZIP;
+                
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
   
 
   Encoding StringToEncoding(const char* encoding)
@@ -1304,8 +1377,7 @@
 
       if (throwIfUnsupported)
       {
-        LOG(ERROR) << s;
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancException(ErrorCode_ParameterOutOfRange, s);
       }
       else
       {
@@ -1441,8 +1513,8 @@
     }
     else
     {
-      LOG(ERROR) << "Unknown modality manufacturer: \"" << manufacturer << "\"";
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Unknown modality manufacturer: \"" + manufacturer + "\"");
     }
 
     if (obsolete)
@@ -1535,6 +1607,84 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
+
+
+  MimeType StringToMimeType(const std::string& mime)
+  {
+    if (mime == MIME_BINARY)
+    {
+      return MimeType_Binary;
+    }
+    else if (mime == MIME_DICOM)
+    {
+      return MimeType_Dicom;
+    }
+    else if (mime == MIME_JPEG)
+    {
+      return MimeType_Jpeg;
+    }
+    else if (mime == MIME_JPEG2000)
+    {
+      return MimeType_Jpeg2000;
+    }
+    else if (mime == MIME_JSON)
+    {
+      return MimeType_Json;
+    }
+    else if (mime == MIME_PDF)
+    {
+      return MimeType_Pdf;
+    }
+    else if (mime == MIME_PNG)
+    {
+      return MimeType_Png;
+    }
+    else if (mime == MIME_XML ||
+             mime == MIME_XML_2)
+    {
+      return MimeType_Xml;
+    }
+    else if (mime == MIME_PLAIN_TEXT)
+    {
+      return MimeType_PlainText;
+    }
+    else if (mime == MIME_PAM)
+    {
+      return MimeType_Pam;
+    }
+    else if (mime == MIME_HTML)
+    {
+      return MimeType_Html;
+    }
+    else if (mime == MIME_GZIP)
+    {
+      return MimeType_Gzip;
+    }
+    else if (mime == MIME_JAVASCRIPT)
+    {
+      return MimeType_JavaScript;
+    }
+    else if (mime == MIME_CSS)
+    {
+      return MimeType_Css;
+    }
+    else if (mime == MIME_WEB_ASSEMBLY)
+    {
+      return MimeType_WebAssembly;
+    }
+    else if (mime == MIME_GIF)
+    {
+      return MimeType_Gif;
+    }
+    else if (mime == MIME_ZIP)
+    {
+      return MimeType_Zip;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
   
 
   unsigned int GetBytesPerPixel(PixelFormat format)
@@ -1836,6 +1986,27 @@
       case ErrorCode_DatabaseUnavailable:
         return HttpStatus_503_ServiceUnavailable;
 
+      case ErrorCode_CreateDicomNotString:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomOverrideTag:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomUseContent:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomNoPayload:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomUseDataUriScheme:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomBadParent:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomParentIsInstance:
+        return HttpStatus_400_BadRequest;
+
       default:
         return HttpStatus_500_InternalServerError;
     }
--- a/Core/Enumerations.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Enumerations.h	Thu Dec 06 15:58:08 2018 +0100
@@ -36,6 +36,7 @@
 #include <string>
 
 
+// Macro "ORTHANC_FORCE_INLINE" forces a function/method to be inlined
 #if defined(_MSC_VER)
 #  define ORTHANC_FORCE_INLINE __forceinline
 #elif defined(__GNUC__) || defined(__clang__) || defined(__EMSCRIPTEN__)
@@ -45,8 +46,65 @@
 #endif
 
 
+// Macros "ORTHANC_OVERRIDE" and "ORTHANC_FINAL" wrap the "override"
+// and "final" keywords introduced in C++11, to do compile-time
+// checking of virtual methods
+#if __cplusplus >= 201103L
+// C++11 is enabled
+#  define ORTHANC_OVERRIDE  override
+#  define ORTHANC_FINAL     final
+#else
+// C++11 is disabled
+#  define ORTHANC_OVERRIDE
+#  define ORTHANC_FINAL
+#endif
+
+
 namespace Orthanc
 {
+  static const char* const URI_SCHEME_PREFIX_BINARY = "data:application/octet-stream;base64,";
+
+  static const char* const MIME_BINARY = "application/octet-stream";
+  static const char* const MIME_JPEG = "image/jpeg";
+  static const char* const MIME_JSON = "application/json";
+  static const char* const MIME_JSON_UTF8 = "application/json; charset=utf-8";
+  static const char* const MIME_PDF = "application/pdf";
+  static const char* const MIME_PNG = "image/png";
+  static const char* const MIME_XML = "application/xml";
+  static const char* const MIME_XML_UTF8 = "application/xml; charset=utf-8";
+
+  /**
+   * "No Internet Media Type (aka MIME type, content type) for PBM has
+   * been registered with IANA, but the unofficial value
+   * image/x-portable-arbitrarymap is assigned by this specification,
+   * to be consistent with conventional values for the older Netpbm
+   * formats."  http://netpbm.sourceforge.net/doc/pam.html
+   **/
+  static const char* const MIME_PAM = "image/x-portable-arbitrarymap";
+
+
+  enum MimeType
+  {
+    MimeType_Binary,
+    MimeType_Dicom,
+    MimeType_Html,
+    MimeType_Jpeg,
+    MimeType_Jpeg2000,
+    MimeType_Json,
+    MimeType_Pam,
+    MimeType_Pdf,
+    MimeType_PlainText,
+    MimeType_Png,
+    MimeType_Xml,
+    MimeType_Gzip,
+    MimeType_JavaScript,
+    MimeType_Css,
+    MimeType_WebAssembly,
+    MimeType_Gif,
+    MimeType_Zip
+  };
+
+  
   enum Endianness
   {
     Endianness_Unknown,
@@ -662,6 +720,8 @@
 
   const char* EnumerationToString(JobState state);
 
+  const char* EnumerationToString(MimeType mime);
+
   Encoding StringToEncoding(const char* encoding);
 
   ResourceType StringToResourceType(const char* type);
@@ -682,6 +742,8 @@
   JobState StringToJobState(const std::string& state);
   
   RequestOrigin StringToRequestOrigin(const std::string& origin);
+
+  MimeType StringToMimeType(const std::string& mime);
   
   unsigned int GetBytesPerPixel(PixelFormat format);
 
--- a/Core/FileStorage/StorageAccessor.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/FileStorage/StorageAccessor.h	Thu Dec 06 15:58:08 2018 +0100
@@ -110,10 +110,24 @@
 #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
     void AnswerFile(HttpOutput& output,
                     const FileInfo& info,
+                    MimeType mime)
+    {
+      AnswerFile(output, info, EnumerationToString(mime));
+    }
+
+    void AnswerFile(HttpOutput& output,
+                    const FileInfo& info,
                     const std::string& mime);
 
     void AnswerFile(RestApiOutput& output,
                     const FileInfo& info,
+                    MimeType mime)
+    {
+      AnswerFile(output, info, EnumerationToString(mime));
+    }
+
+    void AnswerFile(RestApiOutput& output,
+                    const FileInfo& info,
                     const std::string& mime);
 #endif
   };
--- a/Core/HttpClient.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/HttpClient.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -77,8 +77,9 @@
 #if ORTHANC_ENABLE_SSL == 1
     return GetHttpStatus(curl_easy_perform(curl), curl, status);
 #else
-    LOG(ERROR) << "Orthanc was compiled without SSL support, cannot make HTTPS request";
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                    "Orthanc was compiled without SSL support, "
+                                    "cannot make HTTPS request");
 #endif
   }
 }
@@ -220,15 +221,15 @@
   {
     if (code == CURLE_NOT_BUILT_IN)
     {
-      LOG(ERROR) << "Your libcurl does not contain a required feature, "
-                 << "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF";
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Your libcurl does not contain a required feature, "
+                             "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF");
     }
 
     if (code != CURLE_OK)
     {
-      LOG(ERROR) << "libCURL error: " + std::string(curl_easy_strerror(code));
-      throw OrthancException(ErrorCode_NetworkProtocol);
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "libCURL error: " + std::string(curl_easy_strerror(code)));
     }
 
     return code;
@@ -502,8 +503,8 @@
     if (!clientCertificateFile_.empty() &&
         pkcs11Enabled_)
     {
-      LOG(ERROR) << "Cannot enable both client certificates and PKCS#11 authentication";
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Cannot enable both client certificates and PKCS#11 authentication");
     }
 
     if (pkcs11Enabled_)
@@ -517,12 +518,13 @@
       }
       else
       {
-        LOG(ERROR) << "Cannot use PKCS#11 for a HTTPS request, because it has not been initialized";
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "Cannot use PKCS#11 for a HTTPS request, "
+                               "because it has not been initialized");
       }
 #else
-      LOG(ERROR) << "This version of Orthanc is compiled without support for PKCS#11";
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "This version of Orthanc is compiled without support for PKCS#11");
 #endif
     }
     else if (!clientCertificateFile_.empty())
@@ -544,8 +546,9 @@
         CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEY, clientCertificateKeyFile_.c_str()));
       }
 #else
-      LOG(ERROR) << "This version of Orthanc is compiled without OpenSSL support, cannot use HTTPS client authentication";
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "This version of Orthanc is compiled without OpenSSL support, "
+                             "cannot use HTTPS client authentication");
 #endif
     }
 
@@ -836,15 +839,15 @@
 
     if (!SystemToolbox::IsRegularFile(certificateFile))
     {
-      LOG(ERROR) << "Cannot open certificate file: " << certificateFile;
-      throw OrthancException(ErrorCode_InexistentFile);
+      throw OrthancException(ErrorCode_InexistentFile,
+                             "Cannot open certificate file: " + certificateFile);
     }
 
     if (!certificateKeyFile.empty() && 
         !SystemToolbox::IsRegularFile(certificateKeyFile))
     {
-      LOG(ERROR) << "Cannot open key file: " << certificateKeyFile;
-      throw OrthancException(ErrorCode_InexistentFile);
+      throw OrthancException(ErrorCode_InexistentFile,
+                             "Cannot open key file: " + certificateKeyFile);
     }
 
     clientCertificateFile_ = certificateFile;
@@ -862,8 +865,8 @@
               << (pin.empty() ? " (no PIN provided)" : " (PIN is provided)");
     GlobalParameters::GetInstance().InitializePkcs11(module, pin, verbose);    
 #else
-    LOG(ERROR) << "This version of Orthanc is compiled without support for PKCS#11";
-    throw OrthancException(ErrorCode_InternalError);
+    throw OrthancException(ErrorCode_InternalError,
+                           "This version of Orthanc is compiled without support for PKCS#11");
 #endif
   }
 }
--- a/Core/HttpServer/EmbeddedResourceHttpHandler.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/HttpServer/EmbeddedResourceHttpHandler.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -36,6 +36,7 @@
 
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../SystemToolbox.h"
 #include "HttpOutput.h"
 
 #include <stdio.h>
@@ -77,14 +78,14 @@
     }
 
     std::string resourcePath = Toolbox::FlattenUri(uri, baseUri_.size());
-    std::string contentType = Toolbox::AutodetectMimeType(resourcePath);
+    MimeType contentType = SystemToolbox::AutodetectMimeType(resourcePath);
 
     try
     {
       const void* buffer = EmbeddedResources::GetDirectoryResourceBuffer(resourceId_, resourcePath.c_str());
       size_t size = EmbeddedResources::GetDirectoryResourceSize(resourceId_, resourcePath.c_str());
 
-      output.SetContentType(contentType.c_str());
+      output.SetContentType(contentType);
       output.Answer(buffer, size);
     }
     catch (OrthancException&)
--- a/Core/HttpServer/FilesystemHttpHandler.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/HttpServer/FilesystemHttpHandler.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -107,7 +107,7 @@
     s += "  </body>";
     s += "</html>";
 
-    output.SetContentType("text/html");
+    output.SetContentType(MimeType_Html);
     output.Answer(s);
   }
 
--- a/Core/HttpServer/HttpContentNegociation.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/HttpServer/HttpContentNegociation.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -159,8 +159,9 @@
         }
         else
         {
-          LOG(ERROR) << "Quality parameter out of range in a HTTP request (must be between 0 and 1): " << value;
-          throw OrthancException(ErrorCode_BadRequest);
+          throw OrthancException(
+            ErrorCode_BadRequest,
+            "Quality parameter out of range in a HTTP request (must be between 0 and 1): " + value);
         }
       }
     }
--- a/Core/HttpServer/HttpFileSender.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/HttpServer/HttpFileSender.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -36,6 +36,7 @@
 
 #include "../OrthancException.h"
 #include "../Toolbox.h"
+#include "../SystemToolbox.h"
 
 #include <boost/lexical_cast.hpp>
 
@@ -47,7 +48,7 @@
 
     if (contentType_.empty())
     {
-      contentType_ = Toolbox::AutodetectMimeType(filename);
+      contentType_ = SystemToolbox::AutodetectMimeType(filename);
     }
   }
 
@@ -69,7 +70,7 @@
   {
     if (contentType_.empty())
     {
-      return "application/octet-stream";
+      return MIME_BINARY;
     }
     else
     {
--- a/Core/HttpServer/HttpFileSender.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/HttpServer/HttpFileSender.h	Thu Dec 06 15:58:08 2018 +0100
@@ -44,6 +44,11 @@
     std::string filename_;
 
   public:
+    void SetContentType(MimeType contentType)
+    {
+      contentType_ = EnumerationToString(contentType);
+    }
+
     void SetContentType(const std::string& contentType)
     {
       contentType_ = contentType;
--- a/Core/HttpServer/HttpOutput.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/HttpServer/HttpOutput.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -151,8 +151,9 @@
       }
       else
       {
-        LOG(ERROR) << "Because of keep-alive connections, the entire body must be sent at once or Content-Length must be given";
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "Because of keep-alive connections, the entire body must "
+                               "be sent at once or Content-Length must be given");
       }
     }
 
@@ -198,8 +199,8 @@
     if (hasContentLength_ &&
         contentPosition_ + length > contentLength_)
     {
-      LOG(ERROR) << "The body size exceeds what was declared with SetContentSize()";
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "The body size exceeds what was declared with SetContentSize()");
     }
 
     if (length > 0)
@@ -233,15 +234,15 @@
         }
         else
         {
-          LOG(ERROR) << "The body size has not reached what was declared with SetContentSize()";
-          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+          throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                                 "The body size has not reached what was declared with SetContentSize()");
         }
 
         break;
 
       case State_WritingMultipart:
-        LOG(ERROR) << "Cannot invoke CloseBody() with multipart outputs";
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "Cannot invoke CloseBody() with multipart outputs");
 
       case State_Done:
         return;  // Ignore
@@ -296,8 +297,8 @@
         status == HttpStatus_401_Unauthorized ||
         status == HttpStatus_405_MethodNotAllowed)
     {
-      LOG(ERROR) << "Please use the dedicated methods to this HTTP status code in HttpOutput";
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Please use the dedicated methods to this HTTP status code in HttpOutput");
     }
     
     stateMachine_.SetHttpStatus(status);
@@ -322,6 +323,7 @@
     stateMachine_.SendBody(NULL, 0);
   }
 
+  
   void HttpOutput::Answer(const void* buffer, 
                           size_t length)
   {
@@ -407,8 +409,8 @@
 
     if (keepAlive_)
     {
-      LOG(ERROR) << "Multipart answers are not implemented together with keep-alive connections";
-      throw OrthancException(ErrorCode_NotImplemented);
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "Multipart answers are not implemented together with keep-alive connections");
     }
 
     if (state_ != State_WritingHeader)
@@ -432,9 +434,9 @@
     {
       if (!Toolbox::StartsWith(*it, "Set-Cookie: "))
       {
-        LOG(ERROR) << "The only headers that can be set in multipart answers "
-                   << "are Set-Cookie (here: " << *it << " is set)";
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "The only headers that can be set in multipart answers "
+                               "are Set-Cookie (here: " + *it + " is set)");
       }
 
       header += *it;
@@ -589,7 +591,7 @@
     std::string contentType = stream.GetContentType();
     if (contentType.empty())
     {
-      contentType = "application/octet-stream";
+      contentType = MIME_BINARY;
     }
 
     stateMachine_.SetContentType(contentType.c_str());
--- a/Core/HttpServer/HttpOutput.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/HttpServer/HttpOutput.h	Thu Dec 06 15:58:08 2018 +0100
@@ -165,9 +165,14 @@
       SendStatus(status, message.c_str(), message.size());
     }
 
-    void SetContentType(const char* contentType)
+    void SetContentType(MimeType contentType)
     {
-      stateMachine_.SetContentType(contentType);
+      stateMachine_.SetContentType(EnumerationToString(contentType));
+    }
+    
+    void SetContentType(const std::string& contentType)
+    {
+      stateMachine_.SetContentType(contentType.c_str());
     }
 
     void SetContentFilename(const char* filename)
--- a/Core/HttpServer/MongooseServer.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/HttpServer/MongooseServer.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -813,8 +813,8 @@
         // Now convert native exceptions as OrthancException
         catch (boost::bad_lexical_cast&)
         {
-          LOG(ERROR) << "Syntax error in some user-supplied data";
-          throw OrthancException(ErrorCode_BadParameterType);
+          throw OrthancException(ErrorCode_BadParameterType,
+                                 "Syntax error in some user-supplied data");
         }
         catch (std::runtime_error&)
         {
@@ -823,13 +823,13 @@
         }
         catch (std::bad_alloc&)
         {
-          LOG(ERROR) << "The server hosting Orthanc is running out of memory";
-          throw OrthancException(ErrorCode_NotEnoughMemory);
+          throw OrthancException(ErrorCode_NotEnoughMemory,
+                                 "The server hosting Orthanc is running out of memory");
         }
         catch (...)
         {
-          LOG(ERROR) << "An unhandled exception was generated inside the HTTP server";
-          throw OrthancException(ErrorCode_InternalError);
+          throw OrthancException(ErrorCode_InternalError,
+                                 "An unhandled exception was generated inside the HTTP server");
         }
       }
       catch (OrthancException& e)
@@ -919,6 +919,7 @@
     httpCompression_ = true;
     exceptionFormatter_ = NULL;
     realm_ = ORTHANC_REALM;
+    threadsCount_ = 50;  // Default value in mongoose
 
 #if ORTHANC_ENABLE_SSL == 1
     // Check for the Heartbleed exploit
@@ -957,6 +958,7 @@
     if (!IsRunning())
     {
       std::string port = boost::lexical_cast<std::string>(port_);
+      std::string numThreads = boost::lexical_cast<std::string>(threadsCount_);
 
       if (ssl_)
       {
@@ -975,6 +977,9 @@
         // https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md#enable_keep_alive-no
         "keep_alive_timeout_ms", (keepAlive_ ? "500" : "0"),
 #endif
+
+        // Set the number of threads
+        "num_threads", numThreads.c_str(),
         
         // Set the SSL certificate, if any. This must be the last option.
         ssl_ ? "ssl_certificate" : NULL,
@@ -1125,4 +1130,16 @@
 
     return *handler_;
   }
+
+
+  void MongooseServer::SetThreadsCount(unsigned int threads)
+  {
+    if (threads <= 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+    Stop();
+    threadsCount_ = threads;
+  }
 }
--- a/Core/HttpServer/MongooseServer.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/HttpServer/MongooseServer.h	Thu Dec 06 15:58:08 2018 +0100
@@ -96,6 +96,7 @@
     bool httpCompression_;
     IHttpExceptionFormatter* exceptionFormatter_;
     std::string realm_;
+    unsigned int threadsCount_;
   
     bool IsRunning() const;
 
@@ -198,5 +199,12 @@
     {
       realm_ = realm;
     }
+
+    void SetThreadsCount(unsigned int threads);
+
+    unsigned int GetThreadsCount() const
+    {
+      return threadsCount_;
+    }
   };
 }
--- a/Core/Images/Font.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Images/Font.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -42,8 +42,10 @@
 #  include "../SystemToolbox.h"
 #endif
 
+#include "../OrthancException.h"
 #include "../Toolbox.h"
-#include "../OrthancException.h"
+#include "Image.h"
+#include "ImageProcessing.h"
 
 #include <stdio.h>
 #include <memory>
@@ -151,6 +153,16 @@
 #endif
 
 
+#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1
+  void Font::LoadFromResource(EmbeddedResources::FileResourceId resource)
+  {
+    std::string content;
+    EmbeddedResources::GetFileResource(content, resource);
+    LoadFromMemory(content);
+  }
+#endif
+
+
   static unsigned int MyMin(unsigned int a, 
                             unsigned int b)
   {
@@ -335,4 +347,83 @@
     DrawInternal(target, utf8, x, y, color);
   }
 
+
+  void Font::ComputeTextExtent(unsigned int& width,
+                               unsigned int& height,
+                               const std::string& utf8) const
+  {
+    width = 0;
+    height = 0;
+    
+#if ORTHANC_ENABLE_LOCALE == 1
+    std::string s = Toolbox::ConvertFromUtf8(utf8, Encoding_Latin1);
+#else
+    // If the locale support is disabled, simply drop non-ASCII
+    // characters from the source UTF-8 string
+    std::string s = Toolbox::ConvertToAscii(utf8);
+#endif
+
+    // Compute the text extent
+    unsigned int x = 0;
+    unsigned int y = 0;
+    
+    for (size_t i = 0; i < s.size(); i++)
+    {
+      if (s[i] == '\n')
+      {
+        // Go to the next line
+        x = 0;
+        y += (maxHeight_ + 1);
+      }
+      else
+      {
+        Characters::const_iterator c = characters_.find(s[i]);
+        if (c != characters_.end())
+        {
+          x += c->second->advance_;
+
+          unsigned int bottom = y + c->second->top_ + c->second->height_;
+          if (bottom > height)
+          {
+            height = bottom;
+          }
+          
+          if (x > width)
+          {
+            width = x;
+          }
+        }
+      }
+    }
+  }
+
+
+  ImageAccessor* Font::Render(const std::string& utf8,
+                              PixelFormat format,
+                              uint8_t r,
+                              uint8_t g,
+                              uint8_t b) const
+  {
+    unsigned int width, height;
+    ComputeTextExtent(width, height, utf8);
+    
+    std::auto_ptr<ImageAccessor>  target(new Image(format, width, height, false));
+    ImageProcessing::Set(*target, 0, 0, 0, 255);
+    Draw(*target, utf8, 0, 0, r, g, b);
+
+    return target.release();
+  }
+
+
+  ImageAccessor* Font::RenderAlpha(const std::string& utf8) const
+  {
+    unsigned int width, height;
+    ComputeTextExtent(width, height, utf8);
+
+    std::auto_ptr<ImageAccessor>  target(new Image(PixelFormat_Grayscale8, width, height, false));
+    ImageProcessing::Set(*target, 0);
+    Draw(*target, utf8, 0, 0, 255);
+
+    return target.release();
+  }
 }
--- a/Core/Images/Font.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Images/Font.h	Thu Dec 06 15:58:08 2018 +0100
@@ -33,6 +33,14 @@
 
 #pragma once
 
+#if !defined(ORTHANC_HAS_EMBEDDED_RESOURCES)
+#  error Macro ORTHANC_HAS_EMBEDDED_RESOURCES must be defined
+#endif
+
+#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1
+#  include <EmbeddedResources.h>   // Autogenerated file
+#endif
+
 #include "ImageAccessor.h"
 
 #include <stdint.h>
@@ -88,6 +96,10 @@
     void LoadFromFile(const std::string& path);
 #endif
 
+#if ORTHANC_HAS_EMBEDDED_RESOURCES == 1
+    void LoadFromResource(EmbeddedResources::FileResourceId resource);
+#endif
+
     const std::string& GetName() const
     {
       return name_;
@@ -111,5 +123,17 @@
               uint8_t r,
               uint8_t g,
               uint8_t b) const;
+
+    void ComputeTextExtent(unsigned int& width,
+                           unsigned int& height,
+                           const std::string& utf8) const;
+
+    ImageAccessor* Render(const std::string& utf8,
+                          PixelFormat format,
+                          uint8_t r,
+                          uint8_t g,
+                          uint8_t b) const;
+
+    ImageAccessor* RenderAlpha(const std::string& utf8) const;
   };
 }
--- a/Core/Images/FontRegistry.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Images/FontRegistry.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -88,4 +88,18 @@
       return *fonts_[i];
     }
   }
+
+  const Font* FontRegistry::FindFont(const std::string& fontName) const
+  {
+    for (Fonts::const_iterator it = fonts_.begin(); it != fonts_.end(); it++)
+    {
+      if ((*it)->GetName() == fontName)
+      {
+        return *it;
+      }
+    }
+
+    return NULL;
+  }
+
 }
--- a/Core/Images/FontRegistry.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Images/FontRegistry.h	Thu Dec 06 15:58:08 2018 +0100
@@ -71,5 +71,7 @@
     }
 
     const Font& GetFont(size_t i) const;
+
+    const Font* FindFont(const std::string& fontName) const;
   };
 }
--- a/Core/Images/ImageAccessor.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Images/ImageAccessor.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -107,11 +107,8 @@
   {
     if (readOnly_)
     {
-#if ORTHANC_ENABLE_LOGGING == 1
-      LOG(ERROR) << "Trying to write on a read-only image";
-#endif
-
-      throw OrthancException(ErrorCode_ReadOnly);
+      throw OrthancException(ErrorCode_ReadOnly,
+                             "Trying to write to a read-only image");
     }
 
     return buffer_;
@@ -135,11 +132,8 @@
   {
     if (readOnly_)
     {
-#if ORTHANC_ENABLE_LOGGING == 1
-      LOG(ERROR) << "Trying to write on a read-only image";
-#endif
-
-      throw OrthancException(ErrorCode_ReadOnly);
+      throw OrthancException(ErrorCode_ReadOnly,
+                             "Trying to write to a read-only image");
     }
 
     if (buffer_ != NULL)
@@ -299,10 +293,8 @@
   {
     if (readOnly_)
     {
-#if ORTHANC_ENABLE_LOGGING == 1
-      LOG(ERROR) << "Trying to modify the format of a read-only image";
-#endif
-      throw OrthancException(ErrorCode_ReadOnly);
+      throw OrthancException(ErrorCode_ReadOnly,
+                             "Trying to modify the format of a read-only image");
     }
 
     if (::Orthanc::GetBytesPerPixel(format) != ::Orthanc::GetBytesPerPixel(format_))
--- a/Core/Images/ImageProcessing.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Images/ImageProcessing.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -53,12 +53,15 @@
     const TargetType minValue = std::numeric_limits<TargetType>::min();
     const TargetType maxValue = std::numeric_limits<TargetType>::max();
 
-    for (unsigned int y = 0; y < source.GetHeight(); y++)
+    const unsigned int width = source.GetWidth();
+    const unsigned int height = source.GetHeight();
+    
+    for (unsigned int y = 0; y < height; y++)
     {
       TargetType* t = reinterpret_cast<TargetType*>(target.GetRow(y));
       const SourceType* s = reinterpret_cast<const SourceType*>(source.GetConstRow(y));
 
-      for (unsigned int x = 0; x < source.GetWidth(); x++, t++, s++)
+      for (unsigned int x = 0; x < width; x++, t++, s++)
       {
         if (static_cast<int32_t>(*s) < static_cast<int32_t>(minValue))
         {
@@ -83,12 +86,15 @@
   {
     assert(sizeof(float) == 4);
 
-    for (unsigned int y = 0; y < source.GetHeight(); y++)
+    const unsigned int width = source.GetWidth();
+    const unsigned int height = source.GetHeight();
+    
+    for (unsigned int y = 0; y < height; y++)
     {
       float* t = reinterpret_cast<float*>(target.GetRow(y));
       const SourceType* s = reinterpret_cast<const SourceType*>(source.GetConstRow(y));
 
-      for (unsigned int x = 0; x < source.GetWidth(); x++, t++, s++)
+      for (unsigned int x = 0; x < width; x++, t++, s++)
       {
         *t = static_cast<float>(*s);
       }
@@ -96,6 +102,30 @@
   }
 
 
+  template <PixelFormat TargetFormat>
+  static void ConvertFloatToGrayscale(ImageAccessor& target,
+                                      const ImageAccessor& source)
+  {
+    typedef typename PixelTraits<TargetFormat>::PixelType  TargetType;
+    
+    assert(sizeof(float) == 4);
+
+    const unsigned int width = source.GetWidth();
+    const unsigned int height = source.GetHeight();
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      TargetType* q = reinterpret_cast<TargetType*>(target.GetRow(y));
+      const float* p = reinterpret_cast<const float*>(source.GetConstRow(y));
+
+      for (unsigned int x = 0; x < width; x++, p++, q++)
+      {
+        PixelTraits<TargetFormat>::FloatToPixel(*q, *p);
+      }
+    }
+  }
+
+
   template <typename TargetType>
   static void ConvertColorToGrayscale(ImageAccessor& target,
                                       const ImageAccessor& source)
@@ -105,12 +135,15 @@
     const TargetType minValue = std::numeric_limits<TargetType>::min();
     const TargetType maxValue = std::numeric_limits<TargetType>::max();
 
-    for (unsigned int y = 0; y < source.GetHeight(); y++)
+    const unsigned int width = source.GetWidth();
+    const unsigned int height = source.GetHeight();
+    
+    for (unsigned int y = 0; y < height; y++)
     {
       TargetType* t = reinterpret_cast<TargetType*>(target.GetRow(y));
       const uint8_t* s = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
 
-      for (unsigned int x = 0; x < source.GetWidth(); x++, t++, s += 3)
+      for (unsigned int x = 0; x < width; x++, t++, s += 3)
       {
         // Y = 0.2126 R + 0.7152 G + 0.0722 B
         int32_t v = (2126 * static_cast<int32_t>(s[0]) +
@@ -134,17 +167,48 @@
   }
 
 
+  static void MemsetZeroInternal(ImageAccessor& image)
+  {      
+    const unsigned int height = image.GetHeight();
+    const size_t lineSize = image.GetBytesPerPixel() * image.GetWidth();
+    const size_t pitch = image.GetPitch();
+
+    uint8_t *p = reinterpret_cast<uint8_t*>(image.GetBuffer());
+    
+    for (unsigned int y = 0; y < height; y++)
+    {
+      memset(p, 0, lineSize);
+      p += pitch;
+    }
+  }
+
+
   template <typename PixelType>
   static void SetInternal(ImageAccessor& image,
                           int64_t constant)
   {
-    for (unsigned int y = 0; y < image.GetHeight(); y++)
+    if (constant == 0 &&
+        (image.GetFormat() == PixelFormat_Grayscale8 ||
+         image.GetFormat() == PixelFormat_Grayscale16 ||
+         image.GetFormat() == PixelFormat_Grayscale32 ||
+         image.GetFormat() == PixelFormat_Grayscale64 ||
+         image.GetFormat() == PixelFormat_SignedGrayscale16))
     {
-      PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
+      MemsetZeroInternal(image);
+    }
+    else
+    {
+      const unsigned int width = image.GetWidth();
+      const unsigned int height = image.GetHeight();
 
-      for (unsigned int x = 0; x < image.GetWidth(); x++, p++)
+      for (unsigned int y = 0; y < height; y++)
       {
-        *p = static_cast<PixelType>(constant);
+        PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
+
+        for (unsigned int x = 0; x < width; x++, p++)
+        {
+          *p = static_cast<PixelType>(constant);
+        }
       }
     }
   }
@@ -167,9 +231,10 @@
     minValue = std::numeric_limits<PixelType>::max();
     maxValue = std::numeric_limits<PixelType>::min();
 
+    const unsigned int height = source.GetHeight();
     const unsigned int width = source.GetWidth();
 
-    for (unsigned int y = 0; y < source.GetHeight(); y++)
+    for (unsigned int y = 0; y < height; y++)
     {
       const PixelType* p = reinterpret_cast<const PixelType*>(source.GetConstRow(y));
 
@@ -202,11 +267,14 @@
     const int64_t minValue = std::numeric_limits<PixelType>::min();
     const int64_t maxValue = std::numeric_limits<PixelType>::max();
 
-    for (unsigned int y = 0; y < image.GetHeight(); y++)
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+    
+    for (unsigned int y = 0; y < height; y++)
     {
       PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
 
-      for (unsigned int x = 0; x < image.GetWidth(); x++, p++)
+      for (unsigned int x = 0; x < width; x++, p++)
       {
         int64_t v = static_cast<int64_t>(*p) + constant;
 
@@ -240,9 +308,11 @@
 
     const int64_t minValue = std::numeric_limits<PixelType>::min();
     const int64_t maxValue = std::numeric_limits<PixelType>::max();
-    const unsigned int width = image.GetWidth();
 
-    for (unsigned int y = 0; y < image.GetHeight(); y++)
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+
+    for (unsigned int y = 0; y < height; y++)
     {
       PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
 
@@ -334,7 +404,7 @@
       throw OrthancException(ErrorCode_IncompatibleImageFormat);
     }
 
-    unsigned int lineSize = GetBytesPerPixel(source.GetFormat()) * source.GetWidth();
+    unsigned int lineSize = source.GetBytesPerPixel() * source.GetWidth();
 
     assert(source.GetPitch() >= lineSize && target.GetPitch() >= lineSize);
 
@@ -354,6 +424,9 @@
       throw OrthancException(ErrorCode_IncompatibleImageSize);
     }
 
+    const unsigned int width = source.GetWidth();
+    const unsigned int height = source.GetHeight();
+
     if (source.GetFormat() == target.GetFormat())
     {
       Copy(target, source);
@@ -451,14 +524,15 @@
       return;
     }
 
+    
     if (target.GetFormat() == PixelFormat_Grayscale8 &&
         source.GetFormat() == PixelFormat_RGBA32)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++, q++)
+        for (unsigned int x = 0; x < width; x++, q++)
         {
           *q = static_cast<uint8_t>((2126 * static_cast<uint32_t>(p[0]) +
                                      7152 * static_cast<uint32_t>(p[1]) +
@@ -473,11 +547,11 @@
     if (target.GetFormat() == PixelFormat_Grayscale8 &&
         source.GetFormat() == PixelFormat_BGRA32)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++, q++)
+        for (unsigned int x = 0; x < width; x++, q++)
         {
           *q = static_cast<uint8_t>((2126 * static_cast<uint32_t>(p[2]) +
                                      7152 * static_cast<uint32_t>(p[1]) +
@@ -492,11 +566,11 @@
     if (target.GetFormat() == PixelFormat_RGB24 &&
         source.GetFormat() == PixelFormat_RGBA32)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        for (unsigned int x = 0; x < width; x++)
         {
           q[0] = p[0];
           q[1] = p[1];
@@ -512,11 +586,11 @@
     if (target.GetFormat() == PixelFormat_RGB24 &&
         source.GetFormat() == PixelFormat_BGRA32)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        for (unsigned int x = 0; x < width; x++)
         {
           q[0] = p[2];
           q[1] = p[1];
@@ -532,11 +606,11 @@
     if (target.GetFormat() == PixelFormat_RGBA32 &&
         source.GetFormat() == PixelFormat_RGB24)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        for (unsigned int x = 0; x < width; x++)
         {
           q[0] = p[0];
           q[1] = p[1];
@@ -553,11 +627,11 @@
     if (target.GetFormat() == PixelFormat_RGB24 &&
         source.GetFormat() == PixelFormat_Grayscale8)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        for (unsigned int x = 0; x < width; x++)
         {
           q[0] = *p;
           q[1] = *p;
@@ -574,11 +648,11 @@
          target.GetFormat() == PixelFormat_BGRA32) &&
         source.GetFormat() == PixelFormat_Grayscale8)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        for (unsigned int x = 0; x < width; x++)
         {
           q[0] = *p;
           q[1] = *p;
@@ -595,11 +669,11 @@
     if (target.GetFormat() == PixelFormat_BGRA32 &&
         source.GetFormat() == PixelFormat_Grayscale16)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const uint16_t* p = reinterpret_cast<const uint16_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        for (unsigned int x = 0; x < width; x++)
         {
           uint8_t value = (*p < 256 ? *p : 255);
           q[0] = value;
@@ -617,11 +691,11 @@
     if (target.GetFormat() == PixelFormat_BGRA32 &&
         source.GetFormat() == PixelFormat_SignedGrayscale16)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const int16_t* p = reinterpret_cast<const int16_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        for (unsigned int x = 0; x < width; x++)
         {
           uint8_t value;
           if (*p < 0)
@@ -652,11 +726,11 @@
     if (target.GetFormat() == PixelFormat_BGRA32 &&
         source.GetFormat() == PixelFormat_RGB24)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        for (unsigned int x = 0; x < width; x++)
         {
           q[0] = p[2];
           q[1] = p[1];
@@ -673,11 +747,11 @@
     if (target.GetFormat() == PixelFormat_RGB24 &&
         source.GetFormat() == PixelFormat_RGB48)
     {
-      for (unsigned int y = 0; y < source.GetHeight(); y++)
+      for (unsigned int y = 0; y < height; y++)
       {
         const uint16_t* p = reinterpret_cast<const uint16_t*>(source.GetConstRow(y));
         uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < source.GetWidth(); x++)
+        for (unsigned int x = 0; x < width; x++)
         {
           q[0] = p[0] >> 8;
           q[1] = p[1] >> 8;
@@ -690,6 +764,20 @@
       return;
     }
 
+    if (target.GetFormat() == PixelFormat_Grayscale16 &&
+        source.GetFormat() == PixelFormat_Float32)
+    {
+      ConvertFloatToGrayscale<PixelFormat_Grayscale16>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale8 &&
+        source.GetFormat() == PixelFormat_Float32)
+    {
+      ConvertFloatToGrayscale<PixelFormat_Grayscale8>(target, source);
+      return;
+    }
+
     throw OrthancException(ErrorCode_NotImplemented);
   }
 
@@ -701,51 +789,23 @@
     switch (image.GetFormat())
     {
       case PixelFormat_Grayscale8:
-        memset(image.GetBuffer(), static_cast<uint8_t>(value), image.GetPitch() * image.GetHeight());
+        SetInternal<uint8_t>(image, value);
         return;
 
       case PixelFormat_Grayscale16:
-        if (value == 0)
-        {
-          memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight());
-        }
-        else
-        {
-          SetInternal<uint16_t>(image, value);
-        }
+        SetInternal<uint16_t>(image, value);
         return;
 
       case PixelFormat_Grayscale32:
-        if (value == 0)
-        {
-          memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight());
-        }
-        else
-        {
-          SetInternal<uint32_t>(image, value);
-        }
+        SetInternal<uint32_t>(image, value);
         return;
 
       case PixelFormat_Grayscale64:
-        if (value == 0)
-        {
-          memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight());
-        }
-        else
-        {
-          SetInternal<uint64_t>(image, value);
-        }
+        SetInternal<uint64_t>(image, value);
         return;
 
       case PixelFormat_SignedGrayscale16:
-        if (value == 0)
-        {
-          memset(image.GetBuffer(), 0, image.GetPitch() * image.GetHeight());
-        }
-        else
-        {
-          SetInternal<int16_t>(image, value);
-        }
+        SetInternal<int16_t>(image, value);
         return;
 
       case PixelFormat_Float32:
@@ -797,11 +857,14 @@
         throw OrthancException(ErrorCode_NotImplemented);
     }    
 
-    for (unsigned int y = 0; y < image.GetHeight(); y++)
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+
+    for (unsigned int y = 0; y < height; y++)
     {
       uint8_t* q = reinterpret_cast<uint8_t*>(image.GetRow(y));
 
-      for (unsigned int x = 0; x < image.GetWidth(); x++)
+      for (unsigned int x = 0; x < width; x++)
       {
         for (unsigned int i = 0; i < size; i++)
         {
@@ -885,7 +948,7 @@
     {
       case PixelFormat_Float32:
       {
-        assert(sizeof(float) == 32);
+        assert(sizeof(float) == 4);
         float a, b;
         GetMinMaxValueInternal<float>(a, b, image);
         minValue = a;
@@ -1016,15 +1079,18 @@
 
   void ImageProcessing::Invert(ImageAccessor& image)
   {
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+    
     switch (image.GetFormat())
     {
       case PixelFormat_Grayscale8:
       {
-        for (unsigned int y = 0; y < image.GetHeight(); y++)
+        for (unsigned int y = 0; y < height; y++)
         {
           uint8_t* p = reinterpret_cast<uint8_t*>(image.GetRow(y));
 
-          for (unsigned int x = 0; x < image.GetWidth(); x++, p++)
+          for (unsigned int x = 0; x < width; x++, p++)
           {
             *p = 255 - (*p);
           }
--- a/Core/Images/JpegReader.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Images/JpegReader.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -121,8 +121,9 @@
     {
       jpeg_destroy_decompress(&cinfo);
       fclose(fp);
-      LOG(ERROR) << "Error during JPEG decoding: " << jerr.GetMessage();
-      throw OrthancException(ErrorCode_InternalError);
+
+      throw OrthancException(ErrorCode_InternalError,
+                             "Error during JPEG decoding: " + jerr.GetMessage());
     }
 
     // Below this line, we are under the scope of a "setjmp"
@@ -159,8 +160,8 @@
     if (setjmp(jerr.GetJumpBuffer())) 
     {
       jpeg_destroy_decompress(&cinfo);
-      LOG(ERROR) << "Error during JPEG decoding: " << jerr.GetMessage();
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Error during JPEG decoding: " + jerr.GetMessage());
     }
 
     // Below this line, we are under the scope of a "setjmp"
--- a/Core/Images/JpegWriter.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Images/JpegWriter.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -149,8 +149,8 @@
        */
       jpeg_destroy_compress(&cinfo);
       fclose(fp);
-      LOG(ERROR) << "Error during JPEG encoding: " << jerr.GetMessage();
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Error during JPEG encoding: " + jerr.GetMessage());
     }
 
     // Do not allocate data on the stack below this line!
@@ -193,8 +193,8 @@
         free(data);
       }
 
-      LOG(ERROR) << "Error during JPEG encoding: " << jerr.GetMessage();
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Error during JPEG encoding: " + jerr.GetMessage());
     }
 
     // Do not allocate data on the stack below this line!
--- a/Core/Images/PamReader.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Images/PamReader.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -244,4 +244,11 @@
     content_ = buffer;
     ParseContent();
   }
+
+  void PamReader::ReadFromMemory(const void* buffer,
+                                 size_t size)
+  {
+    content_.assign(reinterpret_cast<const char*>(buffer), size);
+    ParseContent();
+  }
 }
--- a/Core/Images/PamReader.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Images/PamReader.h	Thu Dec 06 15:58:08 2018 +0100
@@ -54,5 +54,8 @@
 #endif
 
     void ReadFromMemory(const std::string& buffer);
+
+    void ReadFromMemory(const void* buffer,
+                        size_t size);
   };
 }
--- a/Core/JobsEngine/GenericJobUnserializer.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/JobsEngine/GenericJobUnserializer.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -55,8 +55,8 @@
     }
     else
     {
-      LOG(ERROR) << "Cannot unserialize job of type: " << type;
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot unserialize job of type: " + type);
     }
   }
 
@@ -71,8 +71,8 @@
     }
     else
     {
-      LOG(ERROR) << "Cannot unserialize operation of type: " << type;
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot unserialize operation of type: " + type);
     }
   }
 
@@ -91,8 +91,8 @@
     }
     else
     {
-      LOG(ERROR) << "Cannot unserialize value of type: " << type;
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot unserialize value of type: " + type);
     }
   }
 }
--- a/Core/JobsEngine/IJob.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/JobsEngine/IJob.h	Thu Dec 06 15:58:08 2018 +0100
@@ -65,5 +65,11 @@
     virtual void GetPublicContent(Json::Value& value) = 0;
 
     virtual bool Serialize(Json::Value& value) = 0;
+
+    // This function can only be called if the job has reached its
+    // "success" state
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key) = 0;
   };
 }
--- a/Core/JobsEngine/JobsEngine.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/JobsEngine/JobsEngine.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -156,9 +156,9 @@
   }
 
 
-  JobsEngine::JobsEngine() :
+  JobsEngine::JobsEngine(size_t maxCompletedJobs) :
     state_(State_Setup),
-    registry_(new JobsRegistry),
+    registry_(new JobsRegistry(maxCompletedJobs)),
     threadSleep_(200),
     workers_(1)
   {
@@ -198,7 +198,9 @@
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
 
-    registry_.reset(new JobsRegistry(unserializer, serialized));
+    assert(registry_.get() != NULL);
+    const size_t maxCompletedJobs = registry_->GetMaxCompletedJobs();
+    registry_.reset(new JobsRegistry(unserializer, serialized, maxCompletedJobs));
   }
 
 
--- a/Core/JobsEngine/JobsEngine.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/JobsEngine/JobsEngine.h	Thu Dec 06 15:58:08 2018 +0100
@@ -68,7 +68,7 @@
                        size_t workerIndex);
 
   public:
-    JobsEngine();
+    JobsEngine(size_t maxCompletedJobs);
 
     ~JobsEngine();
 
--- a/Core/JobsEngine/JobsRegistry.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/JobsEngine/JobsRegistry.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -47,7 +47,6 @@
   static const char* JOB = "Job";
   static const char* JOBS = "Jobs";
   static const char* JOBS_REGISTRY = "JobsRegistry";
-  static const char* MAX_COMPLETED_JOBS = "MaxCompletedJobs";
   static const char* CREATION_TIME = "CreationTime";
   static const char* LAST_CHANGE_TIME = "LastChangeTime";
   static const char* RUNTIME = "Runtime";
@@ -264,7 +263,7 @@
         // as a "RunningJob" instance is running. We do not use a
         // mutex at the "JobHandler" level, as serialization would be
         // blocked while a step in the job is running. Instead, we
-        // save a snapshot of the serialized job.
+        // save a snapshot of the serialized job. (*)
 
         if (lastStatus_.HasSerialized())
         {
@@ -292,7 +291,7 @@
       }
       else
       {
-        LOG(INFO) << "Job backup is not supported for job of type: " << jobType_;
+        VLOG(1) << "Job backup is not supported for job of type: " << jobType_;
         return false;
       }
     }
@@ -435,20 +434,19 @@
 
   void JobsRegistry::ForgetOldCompletedJobs()
   {
-    if (maxCompletedJobs_ != 0)
+    while (completedJobs_.size() > maxCompletedJobs_)
     {
-      while (completedJobs_.size() > maxCompletedJobs_)
-      {
-        assert(completedJobs_.front() != NULL);
+      assert(completedJobs_.front() != NULL);
+
+      std::string id = completedJobs_.front()->GetId();
+      assert(jobsIndex_.find(id) != jobsIndex_.end());
 
-        std::string id = completedJobs_.front()->GetId();
-        assert(jobsIndex_.find(id) != jobsIndex_.end());
+      jobsIndex_.erase(id);
+      delete(completedJobs_.front());
+      completedJobs_.pop_front();
+    }
 
-        jobsIndex_.erase(id);
-        delete(completedJobs_.front());
-        completedJobs_.pop_front();
-      }
-    }
+    CheckInvariants();
   }
 
 
@@ -458,26 +456,48 @@
     job.SetState(success ? JobState_Success : JobState_Failure);
 
     completedJobs_.push_back(&job);
-    ForgetOldCompletedJobs();
-
     someJobComplete_.notify_all();
   }
 
 
   void JobsRegistry::MarkRunningAsCompleted(JobHandler& job,
-                                            bool success)
+                                            CompletedReason reason)
   {
-    LOG(INFO) << "Job has completed with " << (success ? "success" : "failure")
-              << ": " << job.GetId();
+    const char* tmp;
+
+    switch (reason)
+    {
+      case CompletedReason_Success:
+        tmp = "success";
+        break;
+
+      case CompletedReason_Failure:
+        tmp = "success";
+        break;
+
+      case CompletedReason_Canceled:
+        tmp = "cancel";
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    LOG(INFO) << "Job has completed with " << tmp << ": " << job.GetId();
 
     CheckInvariants();
 
     assert(job.GetState() == JobState_Running);
-    SetCompletedJob(job, success);
+    SetCompletedJob(job, reason == CompletedReason_Success);
+
+    if (reason == CompletedReason_Canceled)
+    {
+      job.SetLastErrorCode(ErrorCode_CanceledJob);
+    }
 
     if (observer_ != NULL)
     {
-      if (success)
+      if (reason == CompletedReason_Success)
       {
         observer_->SignalJobSuccess(job.GetId());
       }
@@ -487,7 +507,9 @@
       }
     }
 
-    CheckInvariants();
+    // WARNING: The following call might make "job" invalid if the job
+    // history size is empty
+    ForgetOldCompletedJobs();
   }
 
 
@@ -558,8 +580,14 @@
 
     maxCompletedJobs_ = n;
     ForgetOldCompletedJobs();
+  }
 
+
+  size_t JobsRegistry::GetMaxCompletedJobs()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
     CheckInvariants();
+    return maxCompletedJobs_;
   }
 
 
@@ -603,18 +631,45 @@
   }
 
 
+  bool JobsRegistry::GetJobOutput(std::string& output,
+                                  MimeType& mime,
+                                  const std::string& job,
+                                  const std::string& key)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::const_iterator found = jobsIndex_.find(job);
+
+    if (found == jobsIndex_.end())
+    {
+      return false;
+    }
+    else
+    {
+      const JobHandler& handler = *found->second;
+
+      if (handler.GetState() == JobState_Success)
+      {
+        return handler.GetJob().GetOutput(output, mime, key);
+      }
+      else
+      {
+        return false;
+      }
+    }
+  }
+
+
   void JobsRegistry::SubmitInternal(std::string& id,
-                                    JobHandler* handlerRaw,
-                                    bool keepLastChangeTime)
+                                    JobHandler* handler)
   {
-    if (handlerRaw == NULL)
+    if (handler == NULL)
     {
       throw OrthancException(ErrorCode_NullPointer);
     }
     
-    std::auto_ptr<JobHandler>  handler(handlerRaw);
-
-    boost::posix_time::ptime lastChangeTime = handler->GetLastStateChangeTime();
+    std::auto_ptr<JobHandler>  protection(handler);
 
     {
       boost::mutex::scoped_lock lock(mutex_);
@@ -623,13 +678,15 @@
       id = handler->GetId();
       int priority = handler->GetPriority();
 
+      jobsIndex_.insert(std::make_pair(id, protection.release()));
+
       switch (handler->GetState())
       {
         case JobState_Pending:
         case JobState_Retry:
         case JobState_Running:
           handler->SetState(JobState_Pending);
-          pendingJobs_.push(handler.get());
+          pendingJobs_.push(handler);
           pendingJobAvailable_.notify_one();
           break;
  
@@ -645,18 +702,13 @@
           break;
         
         default:
-          LOG(ERROR) << "A job should not be loaded from state: "
-                     << EnumerationToString(handler->GetState());
-          throw OrthancException(ErrorCode_InternalError);
+        {
+          std::string details = ("A job should not be loaded from state: " +
+                                 std::string(EnumerationToString(handler->GetState())));
+          throw OrthancException(ErrorCode_InternalError, details);
+        }
       }
 
-      if (keepLastChangeTime)
-      {
-        handler->SetLastStateChangeTime(lastChangeTime);
-      }
-    
-      jobsIndex_.insert(std::make_pair(id, handler.release()));
-
       LOG(INFO) << "New job submitted with priority " << priority << ": " << id;
 
       if (observer_ != NULL)
@@ -664,7 +716,9 @@
         observer_->SignalJobSubmitted(id);
       }
 
-      CheckInvariants();
+      // WARNING: The following call might make "handler" invalid if
+      // the job history size is empty
+      ForgetOldCompletedJobs();
     }
   }
 
@@ -673,7 +727,7 @@
                             IJob* job,        // Takes ownership
                             int priority)
   {
-    SubmitInternal(id, new JobHandler(job, priority), false);
+    SubmitInternal(id, new JobHandler(job, priority));
   }
 
 
@@ -681,7 +735,7 @@
                             int priority)
   {
     std::string id;
-    SubmitInternal(id, new JobHandler(job, priority), false);
+    SubmitInternal(id, new JobHandler(job, priority));
   }
 
 
@@ -904,7 +958,10 @@
           throw OrthancException(ErrorCode_InternalError);
       }
 
-      CheckInvariants();
+      // WARNING: The following call might make "handler" invalid if
+      // the job history size is empty
+      ForgetOldCompletedJobs();
+
       return true;
     }
   }
@@ -1091,17 +1148,12 @@
       switch (targetState_)
       {
         case JobState_Failure:
-          registry_.MarkRunningAsCompleted(*handler_, false);
-
-          if (canceled_)
-          {
-            handler_->SetLastErrorCode(ErrorCode_CanceledJob);
-          }
-          
+          registry_.MarkRunningAsCompleted
+            (*handler_, canceled_ ? CompletedReason_Canceled : CompletedReason_Failure);
           break;
 
         case JobState_Success:
-          registry_.MarkRunningAsCompleted(*handler_, true);
+          registry_.MarkRunningAsCompleted(*handler_, CompletedReason_Success);
           break;
 
         case JobState_Paused:
@@ -1293,7 +1345,6 @@
 
     target = Json::objectValue;
     target[TYPE] = JOBS_REGISTRY;
-    target[MAX_COMPLETED_JOBS] = static_cast<unsigned int>(maxCompletedJobs_);
     target[JOBS] = Json::objectValue;
     
     for (JobsIndex::const_iterator it = jobsIndex_.begin(); 
@@ -1309,7 +1360,9 @@
 
 
   JobsRegistry::JobsRegistry(IJobUnserializer& unserializer,
-                             const Json::Value& s) :
+                             const Json::Value& s,
+                             size_t maxCompletedJobs) :
+    maxCompletedJobs_(maxCompletedJobs),
     observer_(NULL)
   {
     if (SerializationToolbox::ReadString(s, TYPE) != JOBS_REGISTRY ||
@@ -1319,17 +1372,28 @@
       throw OrthancException(ErrorCode_BadFileFormat);
     }
 
-    maxCompletedJobs_ = SerializationToolbox::ReadUnsignedInteger(s, MAX_COMPLETED_JOBS);
-
     Json::Value::Members members = s[JOBS].getMemberNames();
 
     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));
-      
+
+      const boost::posix_time::ptime lastChangeTime = job->GetLastStateChangeTime();
+
       std::string id;
-      SubmitInternal(id, job.release(), true);
+      SubmitInternal(id, job.release());
+
+      // Check whether the job has not been removed (which could be
+      // the case if the "maxCompletedJobs_" value gets smaller)
+      JobsIndex::iterator found = jobsIndex_.find(id);
+      if (found != jobsIndex_.end())
+      {
+        // The job still lies in the history: Update the time of its
+        // last change to the time that was serialized
+        assert(found->second != NULL);
+        found->second->SetLastStateChangeTime(lastChangeTime);
+      }
     }
   }
 }
--- a/Core/JobsEngine/JobsRegistry.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/JobsEngine/JobsRegistry.h	Thu Dec 06 15:58:08 2018 +0100
@@ -71,6 +71,13 @@
     };
     
   private:
+    enum CompletedReason
+    {
+      CompletedReason_Success,
+      CompletedReason_Failure,
+      CompletedReason_Canceled
+    };
+    
     class JobHandler;
 
     struct PriorityComparator
@@ -115,7 +122,7 @@
                          bool success);
     
     void MarkRunningAsCompleted(JobHandler& job,
-                                bool success);
+                                CompletedReason reason);
 
     void MarkRunningAsRetry(JobHandler& job,
                             unsigned int timeout);
@@ -130,28 +137,35 @@
     void RemoveRetryJob(JobHandler* handler);
       
     void SubmitInternal(std::string& id,
-                        JobHandler* handler,
-                        bool keepLastChangeTime);
+                        JobHandler* handler);
     
   public:
-    JobsRegistry() :
-      maxCompletedJobs_(10),
+    JobsRegistry(size_t maxCompletedJobs) :
+      maxCompletedJobs_(maxCompletedJobs),
       observer_(NULL)
     {
     }
 
     JobsRegistry(IJobUnserializer& unserializer,
-                 const Json::Value& s);
+                 const Json::Value& s,
+                 size_t maxCompletedJobs);
 
     ~JobsRegistry();
 
     void SetMaxCompletedJobs(size_t i);
     
+    size_t GetMaxCompletedJobs();
+
     void ListJobs(std::set<std::string>& target);
 
     bool GetJobInfo(JobInfo& target,
                     const std::string& id);
 
+    bool GetJobOutput(std::string& output,
+                      MimeType& mime,
+                      const std::string& job,
+                      const std::string& key);
+
     void Serialize(Json::Value& target);
     
     void Submit(std::string& id,
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Thu Dec 06 15:58:08 2018 +0100
@@ -71,6 +71,8 @@
     std::list<IObserver*>             observers_;
     TimeoutDicomConnectionManager     connectionManager_;
 
+    void NotifyDone() const;
+
   public:
     SequenceOfOperationsJob();
 
@@ -96,8 +98,8 @@
 
     public:
       Lock(SequenceOfOperationsJob& that) :
-      that_(that),
-      lock_(that.mutex_)
+        that_(that),
+        lock_(that.mutex_)
       {
       }
 
@@ -145,6 +147,13 @@
 
     virtual bool Serialize(Json::Value& value);
 
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key)
+    {
+      return false;
+    }
+
     void AwakeTrailingSleep()
     {
       operationAdded_.notify_one();
--- a/Core/JobsEngine/SetOfCommandsJob.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/JobsEngine/SetOfCommandsJob.h	Thu Dec 06 15:58:08 2018 +0100
@@ -131,5 +131,12 @@
     virtual void GetPublicContent(Json::Value& value);
     
     virtual bool Serialize(Json::Value& target);
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key)
+    {
+      return false;
+    }
   };
 }
--- a/Core/Lua/LuaContext.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Lua/LuaContext.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -587,8 +587,7 @@
 
       std::string description(lua_tostring(lua_, -1));
       lua_pop(lua_, 1); /* pop error message from the stack */
-      LOG(ERROR) << "Error while executing Lua script: " << description;
-      throw OrthancException(ErrorCode_CannotExecuteLua);
+      throw OrthancException(ErrorCode_CannotExecuteLua, description);
     }
 
     if (output != NULL)
--- a/Core/Lua/LuaFunctionCall.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Lua/LuaFunctionCall.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -105,9 +105,8 @@
           
       std::string description(lua_tostring(context_.lua_, -1));
       lua_pop(context_.lua_, 1); /* pop error message from the stack */
-      LOG(ERROR) << description;
 
-      throw OrthancException(ErrorCode_CannotExecuteLua);
+      throw OrthancException(ErrorCode_CannotExecuteLua, description);
     }
 
     if (lua_gettop(context_.lua_) < numOutputs)
--- a/Core/OrthancException.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/OrthancException.h	Thu Dec 06 15:58:08 2018 +0100
@@ -33,19 +33,39 @@
 
 #pragma once
 
+#include "Enumerations.h"
+#include "Logging.h"
+
 #include <stdint.h>
 #include <string>
-#include "Enumerations.h"
+#include <memory>
 
 namespace Orthanc
 {
   class OrthancException
   {
-  protected:
+  private:
+    OrthancException();  // Forbidden
+    
+    OrthancException& operator= (const OrthancException&);  // Forbidden
+
     ErrorCode  errorCode_;
     HttpStatus httpStatus_;
 
+    // New in Orthanc 1.4.3
+    std::auto_ptr<std::string>  details_;
+    
   public:
+    OrthancException(const OrthancException& other) : 
+      errorCode_(other.errorCode_),
+      httpStatus_(other.httpStatus_)
+    {
+      if (other.details_.get() != NULL)
+      {
+        details_.reset(new std::string(*other.details_));
+      }
+    }
+
     explicit OrthancException(ErrorCode errorCode) : 
       errorCode_(errorCode),
       httpStatus_(ConvertErrorCodeToHttpStatus(errorCode))
@@ -53,12 +73,43 @@
     }
 
     OrthancException(ErrorCode errorCode,
+                     const std::string& details,
+                     bool log = true) :
+      errorCode_(errorCode),
+      httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)),
+      details_(new std::string(details))
+    {
+#if ORTHANC_ENABLE_LOGGING == 1
+      if (log)
+      {
+        LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details;
+      }
+#endif
+    }
+
+    OrthancException(ErrorCode errorCode,
                      HttpStatus httpStatus) :
       errorCode_(errorCode),
       httpStatus_(httpStatus)
     {
     }
 
+    OrthancException(ErrorCode errorCode,
+                     HttpStatus httpStatus,
+                     const std::string& details,
+                     bool log = true) :
+      errorCode_(errorCode),
+      httpStatus_(httpStatus),
+      details_(new std::string(details))
+    {
+#if ORTHANC_ENABLE_LOGGING == 1
+      if (log)
+      {
+        LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details;
+      }
+#endif
+    }
+
     ErrorCode GetErrorCode() const
     {
       return errorCode_;
@@ -73,5 +124,22 @@
     {
       return EnumerationToString(errorCode_);
     }
+
+    bool HasDetails() const
+    {
+      return details_.get() != NULL;
+    }
+
+    const char* GetDetails() const
+    {
+      if (details_.get() == NULL)
+      {
+        return "";
+      }
+      else
+      {
+        return details_->c_str();
+      }
+    }
   };
 }
--- a/Core/Pkcs11.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Pkcs11.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -187,17 +187,17 @@
       ENGINE* engine = ENGINE_new();
       if (!engine)
       {
-        LOG(ERROR) << "Cannot create an OpenSSL engine for PKCS#11";
-        throw OrthancException(ErrorCode_InternalError);
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot create an OpenSSL engine for PKCS#11");
       }
 
       // Create a PKCS#11 context using libp11
       context_ = pkcs11_new();
       if (!context_)
       {
-        LOG(ERROR) << "Cannot create a libp11 context for PKCS#11";
         ENGINE_free(engine);
-        throw OrthancException(ErrorCode_InternalError);
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot create a libp11 context for PKCS#11");
       }
 
       if (!ENGINE_set_id(engine, PKCS11_ENGINE_ID) ||
@@ -223,10 +223,10 @@
           // Make OpenSSL know about our PKCS#11 engine
           !ENGINE_add(engine))
       {
-        LOG(ERROR) << "Cannot initialize the OpenSSL engine for PKCS#11";
         pkcs11_finish(context_);
         ENGINE_free(engine);
-        throw OrthancException(ErrorCode_InternalError);
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot initialize the OpenSSL engine for PKCS#11");
       }
 
       // If the "ENGINE_add" worked, it gets a structural
@@ -253,28 +253,29 @@
     {
       if (pkcs11Initialized_)
       {
-        LOG(ERROR) << "The PKCS#11 engine has already been initialized";
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "The PKCS#11 engine has already been initialized");
       }
 
       if (module.empty() ||
           !SystemToolbox::IsRegularFile(module))
       {
-        LOG(ERROR) << "The PKCS#11 module must be a path to one shared library (DLL or .so)";
-        throw OrthancException(ErrorCode_InexistentFile);
+        throw OrthancException(
+          ErrorCode_InexistentFile,
+          "The PKCS#11 module must be a path to one shared library (DLL or .so)");
       }
 
       ENGINE* engine = LoadEngine();
       if (!engine)
       {
-        LOG(ERROR) << "Cannot create an OpenSSL engine for PKCS#11";
-        throw OrthancException(ErrorCode_InternalError);
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot create an OpenSSL engine for PKCS#11");
       }
 
       if (!ENGINE_ctrl_cmd_string(engine, "MODULE_PATH", module.c_str(), 0))
       {
-        LOG(ERROR) << "Cannot configure the OpenSSL dynamic engine for PKCS#11";
-        throw OrthancException(ErrorCode_InternalError);
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot configure the OpenSSL dynamic engine for PKCS#11");
       }
 
       if (verbose)
@@ -285,14 +286,14 @@
       if (!pin.empty() &&
           !ENGINE_ctrl_cmd_string(engine, "PIN", pin.c_str(), 0)) 
       {
-        LOG(ERROR) << "Cannot set the PIN code for PKCS#11";
-        throw OrthancException(ErrorCode_InternalError);
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot set the PIN code for PKCS#11");
       }
   
       if (!ENGINE_init(engine))
       {
-        LOG(ERROR) << "Cannot initialize the OpenSSL dynamic engine for PKCS#11";
-        throw OrthancException(ErrorCode_InternalError);
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot initialize the OpenSSL dynamic engine for PKCS#11");
       }
 
       LOG(WARNING) << "The PKCS#11 engine has been successfully initialized";
--- a/Core/RestApi/RestApi.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/RestApi/RestApi.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -197,12 +197,12 @@
         Toolbox::TokenizeString(accepted, it->second, ';');
         for (size_t i = 0; i < accepted.size(); i++)
         {
-          if (accepted[i] == "application/xml")
+          if (accepted[i] == MIME_XML)
           {
             wrappedOutput.SetConvertJsonToXml(true);
           }
 
-          if (accepted[i] == "application/json")
+          if (accepted[i] == MIME_JSON)
           {
             wrappedOutput.SetConvertJsonToXml(false);
           }
--- a/Core/RestApi/RestApiOutput.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/RestApi/RestApiOutput.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -96,25 +96,28 @@
 #if ORTHANC_ENABLE_PUGIXML == 1
       std::string s;
       Toolbox::JsonToXml(s, value);
-      output_.SetContentType("application/xml; charset=utf-8");
+
+      output_.SetContentType(MIME_XML_UTF8);
       output_.Answer(s);
 #else
-      LOG(ERROR) << "Orthanc was compiled without XML support";
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Orthanc was compiled without XML support");
 #endif
     }
     else
     {
       Json::StyledWriter writer;
-      output_.SetContentType("application/json; charset=utf-8");
-      output_.Answer(writer.write(value));
+      std::string s = writer.write(value);
+      
+      output_.SetContentType(MIME_JSON_UTF8);      
+      output_.Answer(s);
     }
 
     alreadySent_ = true;
   }
 
   void RestApiOutput::AnswerBuffer(const std::string& buffer,
-                                   const std::string& contentType)
+                                   MimeType contentType)
   {
     AnswerBuffer(buffer.size() == 0 ? NULL : buffer.c_str(),
                  buffer.size(), contentType);
@@ -122,10 +125,10 @@
 
   void RestApiOutput::AnswerBuffer(const void* buffer,
                                    size_t length,
-                                   const std::string& contentType)
+                                   MimeType contentType)
   {
     CheckStatus();
-    output_.SetContentType(contentType.c_str());
+    output_.SetContentType(contentType);
     output_.Answer(buffer, length);
     alreadySent_ = true;
   }
--- a/Core/RestApi/RestApiOutput.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/RestApi/RestApiOutput.h	Thu Dec 06 15:58:08 2018 +0100
@@ -75,11 +75,11 @@
     void AnswerJson(const Json::Value& value);
 
     void AnswerBuffer(const std::string& buffer,
-                      const std::string& contentType);
+                      MimeType contentType);
 
     void AnswerBuffer(const void* buffer,
                       size_t length,
-                      const std::string& contentType);
+                      MimeType contentType);
 
     void SignalError(HttpStatus status);
 
--- a/Core/SerializationToolbox.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/SerializationToolbox.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -47,7 +47,8 @@
           !value.isMember(field.c_str()) ||
           value[field.c_str()].type() != Json::stringValue)
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "String value expected in field: " + field);
       }
       else
       {
@@ -64,7 +65,8 @@
           (value[field.c_str()].type() != Json::intValue &&
            value[field.c_str()].type() != Json::uintValue))
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Integer value expected in field: " + field);
       }
       else
       {
@@ -80,7 +82,8 @@
 
       if (tmp < 0)
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Unsigned integer value expected in field: " + field);
       }
       else
       {
@@ -96,7 +99,8 @@
           !value.isMember(field.c_str()) ||
           value[field.c_str()].type() != Json::booleanValue)
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Boolean value expected in field: " + field);
       }
       else
       {
@@ -113,7 +117,8 @@
           !value.isMember(field.c_str()) ||
           value[field.c_str()].type() != Json::arrayValue)
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "List of strings expected in field: " + field);
       }
 
       const Json::Value& arr = value[field.c_str()];
@@ -124,7 +129,8 @@
       {
         if (arr[i].type() != Json::stringValue)
         {
-          throw OrthancException(ErrorCode_BadFileFormat);        
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "List of strings expected in field: " + field);
         }
         else
         {
@@ -172,7 +178,8 @@
           !value.isMember(field.c_str()) ||
           value[field.c_str()].type() != Json::arrayValue)
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Set of DICOM tags expected in field: " + field);
       }
 
       const Json::Value& arr = value[field.c_str()];
@@ -186,7 +193,8 @@
         if (arr[i].type() != Json::stringValue ||
             !DicomTag::ParseHexadecimal(tag, arr[i].asCString()))
         {
-          throw OrthancException(ErrorCode_BadFileFormat);        
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "Set of DICOM tags expected in field: " + field);
         }
         else
         {
@@ -204,7 +212,8 @@
           !value.isMember(field.c_str()) ||
           value[field.c_str()].type() != Json::objectValue)
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Associative array of strings to strings expected in field: " + field);
       }
 
       const Json::Value& source = value[field.c_str()];
@@ -219,7 +228,8 @@
 
         if (tmp.type() != Json::stringValue)
         {
-          throw OrthancException(ErrorCode_BadFileFormat);        
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "Associative array of string to strings expected in field: " + field);
         }
         else
         {
@@ -237,7 +247,8 @@
           !value.isMember(field.c_str()) ||
           value[field.c_str()].type() != Json::objectValue)
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Associative array of DICOM tags to strings expected in field: " + field);
       }
 
       const Json::Value& source = value[field.c_str()];
@@ -255,7 +266,8 @@
         if (!DicomTag::ParseHexadecimal(tag, members[i].c_str()) ||
             tmp.type() != Json::stringValue)
         {
-          throw OrthancException(ErrorCode_BadFileFormat);        
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "Associative array of DICOM tags to strings expected in field: " + field);
         }
         else
         {
--- a/Core/SharedLibrary.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/SharedLibrary.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -119,8 +119,9 @@
   
     if (result == NULL)
     {
-      LOG(ERROR) << "Shared library does not expose function \"" << name << "\"";
-      throw OrthancException(ErrorCode_SharedLibrary);
+      throw OrthancException(
+        ErrorCode_SharedLibrary,
+        "Shared library does not expose function \"" + name + "\"");
     }
     else
     {
--- a/Core/SystemToolbox.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/SystemToolbox.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -38,6 +38,7 @@
 #if defined(_WIN32)
 #  include <windows.h>
 #  include <process.h>   // For "_spawnvp()" and "_getpid()"
+#  include <stdlib.h>    // For "environ"
 #else
 #  include <unistd.h>    // For "execvp()"
 #  include <sys/wait.h>  // For "waitpid()"
@@ -72,6 +73,51 @@
 #include <boost/thread.hpp>
 
 
+/*=========================================================================
+  The section below comes from the Boost 1.68.0 project:
+  https://github.com/boostorg/program_options/blob/boost-1.68.0/src/parsers.cpp
+  
+  Copyright Vladimir Prus 2002-2004.
+  Distributed under the Boost Software License, Version 1.0.
+  (See accompanying file LICENSE_1_0.txt
+  or copy at http://www.boost.org/LICENSE_1_0.txt)
+  =========================================================================*/
+
+// The 'environ' should be declared in some cases. E.g. Linux man page says:
+// (This variable must be declared in the user program, but is declared in 
+// the header file unistd.h in case the header files came from libc4 or libc5, 
+// and in case they came from glibc and _GNU_SOURCE was defined.) 
+// To be safe, declare it here.
+
+// It appears that on Mac OS X the 'environ' variable is not
+// available to dynamically linked libraries.
+// See: http://article.gmane.org/gmane.comp.lib.boost.devel/103843
+// See: http://lists.gnu.org/archive/html/bug-guile/2004-01/msg00013.html
+#if defined(__APPLE__) && defined(__DYNAMIC__)
+// The proper include for this is crt_externs.h, however it's not
+// available on iOS. The right replacement is not known. See
+// https://svn.boost.org/trac/boost/ticket/5053
+extern "C"
+{
+  extern char ***_NSGetEnviron(void);
+}
+#  define environ (*_NSGetEnviron()) 
+#else
+#  if defined(__MWERKS__)
+#    include <crtl.h>
+#  else
+#    if !defined(_WIN32) || defined(__COMO_VERSION__)
+extern char** environ;
+#    endif
+#  endif
+#endif
+
+
+/*=========================================================================
+  End of section from the Boost 1.68.0 project
+  =========================================================================*/
+
+
 namespace Orthanc
 {
   static bool finish_;
@@ -170,8 +216,8 @@
   {
     if (!IsRegularFile(path))
     {
-      LOG(ERROR) << "The path does not point to a regular file: " << path;
-      throw OrthancException(ErrorCode_RegularFileExpected);
+      throw OrthancException(ErrorCode_RegularFileExpected,
+                             "The path does not point to a regular file: " + path);
     }
 
     boost::filesystem::ifstream f;
@@ -198,8 +244,8 @@
   {
     if (!IsRegularFile(path))
     {
-      LOG(ERROR) << "The path does not point to a regular file: " << path;
-      throw OrthancException(ErrorCode_RegularFileExpected);
+      throw OrthancException(ErrorCode_RegularFileExpected,
+                             "The path does not point to a regular file: " + path);
     }
 
     boost::filesystem::ifstream f;
@@ -438,11 +484,7 @@
     if (pid == -1)
     {
       // Error in fork()
-#if ORTHANC_ENABLE_LOGGING == 1
-      LOG(ERROR) << "Cannot fork a child process";
-#endif
-
-      throw OrthancException(ErrorCode_SystemCommand);
+      throw OrthancException(ErrorCode_SystemCommand, "Cannot fork a child process");
     }
     else if (pid == 0)
     {
@@ -461,11 +503,9 @@
 
     if (status != 0)
     {
-#if ORTHANC_ENABLE_LOGGING == 1
-      LOG(ERROR) << "System command failed with status code " << status;
-#endif
-
-      throw OrthancException(ErrorCode_SystemCommand);
+      throw OrthancException(ErrorCode_SystemCommand,
+                             "System command failed with status code " +
+                             boost::lexical_cast<std::string>(status));
     }
   }
 
@@ -578,4 +618,117 @@
       return threads;
     }
   }
+
+
+  MimeType SystemToolbox::AutodetectMimeType(const std::string& path)
+  {
+    std::string extension = boost::filesystem::extension(path);
+    Toolbox::ToLowerCase(extension);
+
+    // http://en.wikipedia.org/wiki/Mime_types
+    // Text types
+    if (extension == ".txt")
+    {
+      return MimeType_PlainText;
+    }
+    else if (extension == ".html")
+    {
+      return MimeType_Html;
+    }
+    else if (extension == ".xml")
+    {
+      return MimeType_Xml;
+    }
+    else if (extension == ".css")
+    {
+      return MimeType_Css;
+    }
+
+    // Application types
+    else if (extension == ".js")
+    {
+      return MimeType_JavaScript;
+    }
+    else if (extension == ".json")
+    {
+      return MimeType_Json;
+    }
+    else if (extension == ".pdf")
+    {
+      return MimeType_Pdf;
+    }
+    else if (extension == ".wasm")
+    {
+      return MimeType_WebAssembly;
+    }
+
+    // Images types
+    else if (extension == ".jpg" ||
+             extension == ".jpeg")
+    {
+      return MimeType_Jpeg;
+    }
+    else if (extension == ".gif")
+    {
+      return MimeType_Gif;
+    }
+    else if (extension == ".png")
+    {
+      return MimeType_Png;
+    }
+    else if (extension == ".pam")
+    {
+      return MimeType_Pam;
+    }
+    else
+    {
+      return MimeType_Binary;
+    }
+  }
+
+
+  void SystemToolbox::GetEnvironmentVariables(std::map<std::string, std::string>& env)
+  {
+    env.clear();
+    
+    for (char **p = environ; *p != NULL; p++)
+    {
+      std::string v(*p);
+      size_t pos = v.find('=');
+
+      if (pos != std::string::npos)
+      {
+        std::string key = v.substr(0, pos);
+        std::string value = v.substr(pos + 1);
+        env[key] = value;
+      } 
+    }
+  }
+
+
+  std::string SystemToolbox::InterpretRelativePath(const std::string& baseDirectory,
+                                                   const std::string& relativePath)
+  {
+    boost::filesystem::path base(baseDirectory);
+    boost::filesystem::path relative(relativePath);
+
+    /**
+       The following lines should be equivalent to this one: 
+
+       return (base / relative).string();
+
+       However, for some unknown reason, some versions of Boost do not
+       make the proper path resolution when "baseDirectory" is an
+       absolute path. So, a hack is used below.
+    **/
+
+    if (relative.is_absolute())
+    {
+      return relative.string();
+    }
+    else
+    {
+      return (base / relative).string();
+    }
+  }
 }
--- a/Core/SystemToolbox.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/SystemToolbox.h	Thu Dec 06 15:58:08 2018 +0100
@@ -43,6 +43,7 @@
 
 #include "Enumerations.h"
 
+#include <map>
 #include <vector>
 #include <string>
 #include <stdint.h>
@@ -100,5 +101,12 @@
                      bool utc);
 
     unsigned int GetHardwareConcurrency();
+
+    MimeType AutodetectMimeType(const std::string& path);
+
+    void GetEnvironmentVariables(std::map<std::string, std::string>& env);
+
+    std::string InterpretRelativePath(const std::string& baseDirectory,
+                                      const std::string& relativePath);
   }
 }
--- a/Core/Toolbox.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Toolbox.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -40,8 +40,13 @@
 #include <boost/algorithm/string/case_conv.hpp>
 #include <boost/algorithm/string/replace.hpp>
 #include <boost/lexical_cast.hpp>
-#include <boost/regex.hpp> 
-#include <boost/uuid/sha1.hpp>
+#include <boost/regex.hpp>
+
+#if BOOST_VERSION >= 106600
+#  include <boost/uuid/detail/sha1.hpp>
+#else
+#  include <boost/uuid/sha1.hpp>
+#endif
  
 #include <string>
 #include <stdint.h>
@@ -51,6 +56,8 @@
 
 
 #if ORTHANC_ENABLE_MD5 == 1
+// TODO - Could be replaced by <boost/uuid/detail/md5.hpp> starting
+// with Boost >= 1.66.0
 #  include "../Resources/ThirdParty/md5/md5.h"
 #endif
 
@@ -310,55 +317,6 @@
   }
 
 
-  std::string Toolbox::AutodetectMimeType(const std::string& path)
-  {
-    std::string contentType;
-    size_t lastDot = path.rfind('.');
-    size_t lastSlash = path.rfind('/');
-
-    if (lastDot == std::string::npos ||
-        (lastSlash != std::string::npos && lastDot < lastSlash))
-    {
-      // No trailing dot, unable to detect the content type
-    }
-    else
-    {
-      const char* extension = &path[lastDot + 1];
-    
-      // http://en.wikipedia.org/wiki/Mime_types
-      // Text types
-      if (!strcmp(extension, "txt"))
-        contentType = "text/plain";
-      else if (!strcmp(extension, "html"))
-        contentType = "text/html";
-      else if (!strcmp(extension, "xml"))
-        contentType = "text/xml";
-      else if (!strcmp(extension, "css"))
-        contentType = "text/css";
-
-      // Application types
-      else if (!strcmp(extension, "js"))
-        contentType = "application/javascript";
-      else if (!strcmp(extension, "json"))
-        contentType = "application/json";
-      else if (!strcmp(extension, "pdf"))
-        contentType = "application/pdf";
-      else if (!strcmp(extension, "wasm"))
-        contentType = "application/wasm";
-
-      // Images types
-      else if (!strcmp(extension, "jpg") || !strcmp(extension, "jpeg"))
-        contentType = "image/jpeg";
-      else if (!strcmp(extension, "gif"))
-        contentType = "image/gif";
-      else if (!strcmp(extension, "png"))
-        contentType = "image/png";
-    }
-
-    return contentType;
-  }
-
-
   std::string Toolbox::FlattenUri(const UriComponents& components,
                                   size_t fromLevel)
   {
@@ -568,22 +526,25 @@
   std::string Toolbox::ConvertToUtf8(const std::string& source,
                                      Encoding sourceEncoding)
   {
-    if (sourceEncoding == Encoding_Utf8)
-    {
-      // Already in UTF-8: No conversion is required
-      return source;
-    }
-
-    if (sourceEncoding == Encoding_Ascii)
-    {
-      return ConvertToAscii(source);
-    }
-
-    const char* encoding = GetBoostLocaleEncoding(sourceEncoding);
-
+    // The "::skip" flag makes boost skip invalid UTF-8
+    // characters. This can occur in badly-encoded DICOM files.
+    
     try
     {
-      return boost::locale::conv::to_utf<char>(source, encoding);
+      if (sourceEncoding == Encoding_Utf8)
+      {
+        // Already in UTF-8: No conversion is required
+        return boost::locale::conv::utf_to_utf<char>(source, boost::locale::conv::skip);
+      }
+      else if (sourceEncoding == Encoding_Ascii)
+      {
+        return ConvertToAscii(source);
+      }
+      else
+      {
+        const char* encoding = GetBoostLocaleEncoding(sourceEncoding);
+        return boost::locale::conv::to_utf<char>(source, encoding, boost::locale::conv::skip);
+      }
     }
     catch (std::runtime_error&)
     {
@@ -598,22 +559,25 @@
   std::string Toolbox::ConvertFromUtf8(const std::string& source,
                                        Encoding targetEncoding)
   {
-    if (targetEncoding == Encoding_Utf8)
-    {
-      // Already in UTF-8: No conversion is required
-      return source;
-    }
-
-    if (targetEncoding == Encoding_Ascii)
-    {
-      return ConvertToAscii(source);
-    }
-
-    const char* encoding = GetBoostLocaleEncoding(targetEncoding);
-
+    // The "::skip" flag makes boost skip invalid UTF-8
+    // characters. This can occur in badly-encoded DICOM files.
+    
     try
     {
-      return boost::locale::conv::from_utf<char>(source, encoding);
+      if (targetEncoding == Encoding_Utf8)
+      {
+        // Already in UTF-8: No conversion is required.
+        return boost::locale::conv::utf_to_utf<char>(source, boost::locale::conv::skip);
+      }
+      else if (targetEncoding == Encoding_Ascii)
+      {
+        return ConvertToAscii(source);
+      }
+      else
+      {
+        const char* encoding = GetBoostLocaleEncoding(targetEncoding);
+        return boost::locale::conv::from_utf<char>(source, encoding, boost::locale::conv::skip);
+      }
     }
     catch (std::runtime_error&)
     {
@@ -624,6 +588,14 @@
 #endif
 
 
+  static bool IsAsciiCharacter(uint8_t c)
+  {
+    return (c != 0 &&
+            c <= 127 &&
+            (c == '\n' || !iscntrl(c)));
+  }
+
+
   bool Toolbox::IsAsciiString(const void* data,
                               size_t size)
   {
@@ -631,7 +603,7 @@
 
     for (size_t i = 0; i < size; i++, p++)
     {
-      if (*p > 127 || *p == 0 || iscntrl(*p))
+      if (!IsAsciiCharacter(*p))
       {
         return false;
       }
@@ -654,7 +626,7 @@
     result.reserve(source.size() + 1);
     for (size_t i = 0; i < source.size(); i++)
     {
-      if (source[i] <= 127 && source[i] >= 0 && !iscntrl(source[i]))
+      if (IsAsciiCharacter(source[i]))
       {
         result.push_back(source[i]);
       }
@@ -1420,8 +1392,8 @@
     if (!ok &&
         !SetGlobalLocale(NULL))
     {
-      LOG(ERROR) << "Cannot initialize global locale";
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot initialize global locale");
     }
 
   }
@@ -1437,8 +1409,8 @@
   {
     if (globalLocale_.get() == NULL)
     {
-      LOG(ERROR) << "No global locale was set, call Toolbox::InitializeGlobalLocale()";
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No global locale was set, call Toolbox::InitializeGlobalLocale()");
     }
 
     /**
@@ -1468,9 +1440,9 @@
      * "utf_to_utf" in order to convert to/from std::wstring.
      **/
 
-    std::wstring w = boost::locale::conv::utf_to_utf<wchar_t>(source);
+    std::wstring w = boost::locale::conv::utf_to_utf<wchar_t>(source, boost::locale::conv::skip);
     w = boost::algorithm::to_upper_copy<std::wstring>(w, *globalLocale_);
-    return boost::locale::conv::utf_to_utf<char>(w);
+    return boost::locale::conv::utf_to_utf<char>(w, boost::locale::conv::skip);
   }
 #endif
 
@@ -1525,6 +1497,98 @@
 #endif
     return s;
   }
+
+
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+
+    class VariableFormatter
+    {
+    public:
+      typedef std::map<std::string, std::string>   Dictionary;
+
+    private:
+      const Dictionary& dictionary_;
+
+    public:
+      VariableFormatter(const Dictionary& dictionary) :
+        dictionary_(dictionary)
+      {
+      }
+  
+      template<typename Out>
+      Out operator()(const boost::smatch& what,
+                     Out out) const
+      {
+        if (!what[1].str().empty())
+        {
+          // Variable without a default value
+          Dictionary::const_iterator found = dictionary_.find(what[1]);
+    
+          if (found != dictionary_.end())
+          {
+            const std::string& value = found->second;
+            out = std::copy(value.begin(), value.end(), out);
+          }
+        }
+        else
+        {
+          // Variable with a default value
+          std::string key;
+          std::string defaultValue;
+          
+          if (!what[2].str().empty())
+          {
+            key = what[2].str();
+            defaultValue = what[3].str();
+          }
+          else if (!what[4].str().empty())
+          {
+            key = what[4].str();
+            defaultValue = what[5].str();
+          }
+          else if (!what[6].str().empty())
+          {
+            key = what[6].str();
+            defaultValue = what[7].str();
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+
+          Dictionary::const_iterator found = dictionary_.find(key);
+    
+          if (found == dictionary_.end())
+          {
+            out = std::copy(defaultValue.begin(), defaultValue.end(), out);
+          }
+          else
+          {
+            const std::string& value = found->second;
+            out = std::copy(value.begin(), value.end(), out);
+          }
+        }
+    
+        return out;
+      }
+    };
+  }
+
+  
+  std::string Toolbox::SubstituteVariables(const std::string& source,
+                                           const std::map<std::string, std::string>& dictionary)
+  {
+    const boost::regex pattern("\\$\\{([^:]*?)\\}|"                 // ${what[1]}
+                               "\\$\\{([^:]*?):-([^'\"]*?)\\}|"     // ${what[2]:-what[3]}
+                               "\\$\\{([^:]*?):-\"([^\"]*?)\"\\}|"  // ${what[4]:-"what[5]"}
+                               "\\$\\{([^:]*?):-'([^']*?)'\\}");    // ${what[6]:-'what[7]'}
+
+    VariableFormatter formatter(dictionary);
+
+    return boost::regex_replace(source, pattern, formatter);
+  }
 }
 
 
--- a/Core/Toolbox.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/Toolbox.h	Thu Dec 06 15:58:08 2018 +0100
@@ -117,8 +117,6 @@
     bool IsChildUri(const UriComponents& baseUri,
                     const UriComponents& testedUri);
 
-    std::string AutodetectMimeType(const std::string& path);
-
     std::string FlattenUri(const UriComponents& components,
                            size_t fromLevel = 0);
 
@@ -238,6 +236,9 @@
     void FinalizeOpenSsl();
 
     std::string GenerateUuid();
+
+    std::string SubstituteVariables(const std::string& source,
+                                    const std::map<std::string, std::string>& dictionary);
   }
 }
 
--- a/Core/WebServiceParameters.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Core/WebServiceParameters.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -92,8 +92,7 @@
     if (!Toolbox::StartsWith(url, "http://") &&
         !Toolbox::StartsWith(url, "https://"))
     {
-      LOG(ERROR) << "Bad URL: " << url;
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat, "Bad URL: " + url);
     }
 
     // Add trailing slash if needed
@@ -142,8 +141,9 @@
 
     if (certificateKeyPassword.empty())
     {
-      LOG(ERROR) << "The password for the HTTPS certificate is not provided: " << certificateFile;
-      throw OrthancException(ErrorCode_BadFileFormat);      
+      throw OrthancException(
+        ErrorCode_BadFileFormat,
+        "The password for the HTTPS certificate is not provided: " + certificateFile);
     }
 
     certificateFile_ = certificateFile;
@@ -173,8 +173,8 @@
     }
     else if (peer.size() == 2)
     {
-      LOG(ERROR) << "The HTTP password is not provided";
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "The HTTP password is not provided");
     }
     else if (peer.size() == 3)
     {
@@ -364,8 +364,9 @@
   {
     if (IsReservedKey(key))
     {
-      LOG(ERROR) << "Cannot use this reserved key to name an user property: " << key;
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(
+        ErrorCode_ParameterOutOfRange,
+        "Cannot use this reserved key to name an user property: " + key);
     }
     else
     {
@@ -488,15 +489,15 @@
     {
       if (!SystemToolbox::IsRegularFile(certificateFile_))
       {
-        LOG(ERROR) << "Cannot open certificate file: " << certificateFile_;
-        throw OrthancException(ErrorCode_InexistentFile);
+        throw OrthancException(ErrorCode_InexistentFile,
+                               "Cannot open certificate file: " + certificateFile_);
       }
 
       if (!certificateKeyFile_.empty() && 
           !SystemToolbox::IsRegularFile(certificateKeyFile_))
       {
-        LOG(ERROR) << "Cannot open key file: " << certificateKeyFile_;
-        throw OrthancException(ErrorCode_InexistentFile);
+        throw OrthancException(ErrorCode_InexistentFile,
+                               "Cannot open key file: " + certificateKeyFile_);
       }
     }
   }
--- a/NEWS	Thu Oct 18 10:48:11 2018 +0200
+++ b/NEWS	Thu Dec 06 15:58:08 2018 +0100
@@ -6,6 +6,10 @@
 -------
 
 * Possibility to restrict the allowed DICOM commands for each modality
+* The Orthanc configuration file can use environment variables
+* New configuration options:
+  - "DicomModalitiesInDatabase" to store the definitions of modalities in the database
+  - "OrthancPeersInDatabase" to store the definitions of Orthanc peers in the database
 
 Orthanc Explorer
 ----------------
@@ -16,16 +20,37 @@
 REST API
 --------
 
+* API Version has been upgraded to 1.2
+* Asynchronous generation of ZIP archives and DICOM medias
 * New URI: "/studies/.../merge" to merge a study
 * New URI: "/studies/.../split" to split a study
+* POST-ing a DICOM file to "/instances" also answers the patient/study/series ID
+* GET "/modalities/..." now returns a JSON object instead of a JSON array
+* New options to URI "/queries/.../answers": "?expand" and "?simplify"
+* New "Details" field in HTTP answers on error (cf. "HttpDescribeErrors" option)
+
+Plugins
+-------
+
+* New function in the SDK: "OrthancPluginSetHttpErrorDetails()"
 
 Maintenance
 -----------
 
+* "SynchronousCMove" is now "true" by default
 * New modality manufacturer: "GE" for GE Healthcare EA and AW
 * Executing a query/retrieve from the REST API now creates a job
 * Fix: Closing DICOM associations after running query/retrieve from REST API
-* Fix: Allow creation of MONOCHROME1 greyscale images in tools/create-dicom
+* Fix: Allow creation of MONOCHROME1 grayscale images in tools/create-dicom
+* Remove invalid characters from badly-encoded UTF-8 strings (impacts PostgreSQL)
+* Orthanc starts even if jobs from a previous execution cannot be unserialized
+* New CMake option "ENABLE_DCMTK_LOG" to disable logging internal to DCMTK
+* Fix issue 114 (Boost 1.68 doesn't support SHA-1 anymore)
+* Support of "JobsHistorySize" set to zero
+* Upgraded dependencies for static and Windows builds:
+  - boost 1.68.0
+  - lua 5.3.5
+
 
 Version 1.4.2 (2018-09-20)
 ==========================
--- a/OrthancExplorer/explorer.js	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancExplorer/explorer.js	Thu Dec 06 15:58:08 2018 +0100
@@ -839,7 +839,7 @@
     success: function(s) {
       var ancestor = s.RemainingAncestor;
       if (ancestor == null)
-        $.mobile.changePage('#find-patients');
+        $.mobile.changePage('#lookup');
       else
         $.mobile.changePage('#' + ancestor.Type.toLowerCase() + '?uuid=' + ancestor.ID);
     }
--- a/OrthancServer/DatabaseWrapper.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/DatabaseWrapper.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -366,8 +366,8 @@
 
     if (!ok)
     {
-      LOG(ERROR) << "Incompatible version of the Orthanc database: " << tmp;
-      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
+      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
+                             "Incompatible version of the Orthanc database: " + tmp);
     }
 
     signalRemainingAncestor_ = new Internals::SignalRemainingAncestor;
--- a/OrthancServer/DicomInstanceToStore.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/DicomInstanceToStore.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -146,14 +146,18 @@
   }
 
 
-  struct DicomInstanceToStore::PImpl
+  class DicomInstanceToStore::PImpl
   {
-    DicomInstanceOrigin              origin_;
-    SmartContainer<std::string>      buffer_;
-    SmartContainer<ParsedDicomFile>  parsed_;
-    SmartContainer<DicomMap>         summary_;
-    SmartContainer<Json::Value>      json_;
-    MetadataMap                      metadata_;
+  public:
+    DicomInstanceOrigin                  origin_;
+    SmartContainer<std::string>          buffer_;
+    SmartContainer<ParsedDicomFile>      parsed_;
+    SmartContainer<DicomMap>             summary_;
+    SmartContainer<Json::Value>          json_;
+    MetadataMap                          metadata_;
+
+  private:
+    std::auto_ptr<DicomInstanceHasher>  hasher_;
 
     void ComputeMissingInformation()
     {
@@ -184,8 +188,8 @@
         if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), 
                                                  *parsed_.GetContent().GetDcmtkObject().getDataset()))
         {
-          LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer";
-          throw OrthancException(ErrorCode_InternalError);
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Unable to serialize a DICOM file to a memory buffer");
         }
       }
 
@@ -225,6 +229,7 @@
     }
 
 
+  public:
     const char* GetBufferData()
     {
       ComputeMissingInformation();
@@ -284,6 +289,22 @@
     }
 
 
+    DicomInstanceHasher& GetHasher()
+    {
+      if (hasher_.get() == NULL)
+      {
+        hasher_.reset(new DicomInstanceHasher(GetSummary()));
+      }
+
+      if (hasher_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      return *hasher_;
+    }
+
+    
     bool LookupTransferSyntax(std::string& result)
     {
       ComputeMissingInformation();
@@ -396,4 +417,10 @@
   {
     return pimpl_->LookupTransferSyntax(result);
   }
+
+
+  DicomInstanceHasher& DicomInstanceToStore::GetHasher()
+  {
+    return pimpl_->GetHasher();
+  }
 }
--- a/OrthancServer/DicomInstanceToStore.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/DicomInstanceToStore.h	Thu Dec 06 15:58:08 2018 +0100
@@ -33,6 +33,7 @@
 
 #pragma once
 
+#include "../Core/DicomFormat/DicomInstanceHasher.h"
 #include "../Core/DicomFormat/DicomMap.h"
 #include "DicomInstanceOrigin.h"
 #include "ServerEnumerations.h"
@@ -49,7 +50,7 @@
     typedef std::map<std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
 
   private:
-    struct PImpl;
+    class PImpl;
     boost::shared_ptr<PImpl>  pimpl_;
 
   public:
@@ -84,5 +85,7 @@
     const Json::Value& GetJson();
 
     bool LookupTransferSyntax(std::string& result);
+
+    DicomInstanceHasher& GetHasher();
   };
 }
--- a/OrthancServer/LuaScripting.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/LuaScripting.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -34,7 +34,7 @@
 #include "PrecompiledHeadersServer.h"
 #include "LuaScripting.h"
 
-#include "OrthancInitialization.h"
+#include "OrthancConfiguration.h"
 #include "OrthancRestApi/OrthancRestApi.h"
 #include "ServerContext.h"
 
@@ -414,7 +414,11 @@
   int LuaScripting::GetOrthancConfiguration(lua_State *state)
   {
     Json::Value configuration;
-    Configuration::GetConfiguration(configuration);
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      configuration = lock.GetJson();
+    }
 
     LuaContext::GetLuaContext(state).PushJson(configuration);
 
@@ -445,7 +449,12 @@
       }
 
       std::string name = parameters["Modality"].asString();
-      RemoteModalityParameters modality = Configuration::GetModalityUsingSymbolicName(name);
+      RemoteModalityParameters modality;
+
+      {
+        OrthancConfiguration::ReaderLock configLock;
+        modality = configLock.GetConfiguration().GetModalityUsingSymbolicName(name);
+      }
 
       // This is not a C-MOVE: No need to call "StoreScuCommand::SetMoveOriginator()"
       return lock.AddStoreScuOperation(localAet, modality);
@@ -453,17 +462,18 @@
 
     if (operation == "store-peer")
     {
+      OrthancConfiguration::ReaderLock configLock;
       std::string name = parameters["Peer"].asString();
 
       WebServiceParameters peer;
-      if (Configuration::GetOrthancPeer(peer, name))
+      if (configLock.GetConfiguration().LookupOrthancPeer(peer, name))
       {
         return lock.AddStorePeerOperation(peer);
       }
       else
       {
-        LOG(ERROR) << "No peer with symbolic name: " << name;
-        throw OrthancException(ErrorCode_UnknownResource);
+        throw OrthancException(ErrorCode_UnknownResource,
+                               "No peer with symbolic name: " + name);
       }
     }
 
@@ -742,17 +752,19 @@
 
   void LuaScripting::LoadGlobalConfiguration()
   {
+    OrthancConfiguration::ReaderLock configLock;
+
     lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
 
     std::list<std::string> luaScripts;
-    Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts");
+    configLock.GetConfiguration().GetListOfStringsParameter(luaScripts, "LuaScripts");
 
     LuaScripting::Lock lock(*this);
 
     for (std::list<std::string>::const_iterator
            it = luaScripts.begin(); it != luaScripts.end(); ++it)
     {
-      std::string path = Configuration::InterpretStringParameterAsPath(*it);
+      std::string path = configLock.GetConfiguration().InterpretStringParameterAsPath(*it);
       LOG(INFO) << "Installing the Lua scripts from: " << path;
       std::string script;
       SystemToolbox::ReadFile(script, path);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancConfiguration.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -0,0 +1,829 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 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 "OrthancConfiguration.h"
+
+#include "../Core/HttpServer/MongooseServer.h"
+#include "../Core/Logging.h"
+#include "../Core/OrthancException.h"
+#include "../Core/SystemToolbox.h"
+#include "../Core/Toolbox.h"
+
+#include "ServerIndex.h"
+
+
+static const char* const DICOM_MODALITIES = "DicomModalities";
+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";
+
+namespace Orthanc
+{
+  static void AddFileToConfiguration(Json::Value& target,
+                                     const boost::filesystem::path& path)
+  {
+    std::map<std::string, std::string> env;
+    SystemToolbox::GetEnvironmentVariables(env);
+    
+    LOG(WARNING) << "Reading the configuration from: " << path;
+
+    Json::Value config;
+
+    {
+      std::string content;
+      SystemToolbox::ReadFile(content, path.string());
+
+      content = Toolbox::SubstituteVariables(content, env);
+
+      Json::Value tmp;
+      Json::Reader reader;
+      if (!reader.parse(content, tmp) ||
+          tmp.type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_BadJson,
+                               "The configuration file does not follow the JSON syntax: " + path.string());
+      }
+
+      Toolbox::CopyJsonWithoutComments(config, tmp);
+    }
+
+    if (target.size() == 0)
+    {
+      target = config;
+    }
+    else
+    {
+      // Merge the newly-added file with the previous content of "target"
+      Json::Value::Members members = config.getMemberNames();
+      for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
+      {
+        if (target.isMember(members[i]))
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "The configuration section \"" + members[i] +
+                                 "\" is defined in 2 different configuration files");
+        }
+        else
+        {
+          target[members[i]] = config[members[i]];
+        }
+      }
+    }
+  }
+
+    
+  static void ScanFolderForConfiguration(Json::Value& target,
+                                         const char* folder)
+  {
+    using namespace boost::filesystem;
+
+    LOG(WARNING) << "Scanning folder \"" << folder << "\" for configuration files";
+
+    directory_iterator end_it; // default construction yields past-the-end
+    for (directory_iterator it(folder);
+         it != end_it;
+         ++it)
+    {
+      if (!is_directory(it->status()))
+      {
+        std::string extension = boost::filesystem::extension(it->path());
+        Toolbox::ToLowerCase(extension);
+
+        if (extension == ".json")
+        {
+          AddFileToConfiguration(target, it->path().string());
+        }
+      }
+    }
+  }
+
+    
+  static void ReadConfiguration(Json::Value& target,
+                                const char* configurationFile)
+  {
+    target = Json::objectValue;
+
+    if (configurationFile != NULL)
+    {
+      if (!boost::filesystem::exists(configurationFile))
+      {
+        throw OrthancException(ErrorCode_InexistentFile,
+                               "Inexistent path to configuration: " +
+                               std::string(configurationFile));
+      }
+      
+      if (boost::filesystem::is_directory(configurationFile))
+      {
+        ScanFolderForConfiguration(target, configurationFile);
+      }
+      else
+      {
+        AddFileToConfiguration(target, configurationFile);
+      }
+    }
+    else
+    {
+#if ORTHANC_STANDALONE == 1
+      // No default path for the standalone configuration
+      LOG(WARNING) << "Using the default Orthanc configuration";
+      return;
+
+#else
+      // In a non-standalone build, we use the
+      // "Resources/Configuration.json" from the Orthanc source code
+
+      boost::filesystem::path p = ORTHANC_PATH;
+      p /= "Resources";
+      p /= "Configuration.json";
+
+      AddFileToConfiguration(target, p);
+#endif
+    }
+  }
+
+
+  static void CheckAlphanumeric(const std::string& s)
+  {
+    for (size_t j = 0; j < s.size(); j++)
+    {
+      if (!isalnum(s[j]) && 
+          s[j] != '-')
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Only alphanumeric and dash characters are allowed "
+                               "in the names of modalities/peers, but found: " + s);
+      }
+    }
+  }
+
+
+  void OrthancConfiguration::LoadModalitiesFromJson(const Json::Value& source)
+  {
+    modalities_.clear();
+
+    if (source.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Bad format of the \"" + std::string(DICOM_MODALITIES) +
+                             "\" configuration section");
+    }
+
+    Json::Value::Members members = source.getMemberNames();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const std::string& name = members[i];
+      CheckAlphanumeric(name);
+
+      RemoteModalityParameters modality;
+      modality.Unserialize(source[name]);
+      modalities_[name] = modality;
+    }
+  }
+
+
+  void OrthancConfiguration::LoadPeersFromJson(const Json::Value& source)
+  {
+    peers_.clear();
+
+    if (source.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Bad format of the \"" + std::string(ORTHANC_PEERS) +
+                             "\" configuration section");
+    }
+
+    Json::Value::Members members = source.getMemberNames();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const std::string& name = members[i];
+      CheckAlphanumeric(name);
+
+      WebServiceParameters peer;
+      peer.Unserialize(source[name]);
+      peers_[name] = peer;
+    }
+  }
+
+
+  void OrthancConfiguration::LoadModalities()
+  {
+    if (GetBooleanParameter(DICOM_MODALITIES_IN_DB, false))
+    {
+      // Modalities are stored in the database
+      if (serverIndex_ == NULL)
+      {
+        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Modalities, "{}");
+
+        Json::Reader reader;
+        Json::Value modalities;
+        if (reader.parse(property, modalities))
+        {
+          LoadModalitiesFromJson(modalities);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Cannot unserialize the list of modalities from the Orthanc database");
+        }
+      }
+    }
+    else
+    {
+      // Modalities are stored in the configuration files
+      if (json_.isMember(DICOM_MODALITIES))
+      {
+        LoadModalitiesFromJson(json_[DICOM_MODALITIES]);
+      }
+      else
+      {
+        modalities_.clear();
+      }
+    }
+  }
+
+  void OrthancConfiguration::LoadPeers()
+  {
+    if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false))
+    {
+      // Peers are stored in the database
+      if (serverIndex_ == NULL)
+      {
+        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Peers, "{}");
+
+        Json::Reader reader;
+        Json::Value peers;
+        if (reader.parse(property, peers))
+        {
+          LoadPeersFromJson(peers);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Cannot unserialize the list of peers from the Orthanc database");
+        }
+      }
+    }
+    else
+    {
+      // Peers are stored in the configuration files
+      if (json_.isMember(ORTHANC_PEERS))
+      {
+        LoadPeersFromJson(json_[ORTHANC_PEERS]);
+      }
+      else
+      {
+        peers_.clear();
+      }
+    }
+  }
+
+
+  void OrthancConfiguration::SaveModalitiesToJson(Json::Value& target)
+  {
+    target = Json::objectValue;
+
+    for (Modalities::const_iterator it = modalities_.begin(); it != modalities_.end(); ++it)
+    {
+      Json::Value modality;
+      it->second.Serialize(modality, true /* force advanced format */);
+
+      target[it->first] = modality;
+    }
+  }
+
+    
+  void OrthancConfiguration::SavePeersToJson(Json::Value& target)
+  {
+    target = Json::objectValue;
+
+    for (Peers::const_iterator it = peers_.begin(); it != peers_.end(); ++it)
+    {
+      Json::Value peer;
+      it->second.Serialize(peer, 
+                           false /* use simple format if possible */, 
+                           true  /* include passwords */);
+
+      target[it->first] = peer;
+    }
+  }  
+    
+    
+  void OrthancConfiguration::SaveModalities()
+  {
+    if (GetBooleanParameter(DICOM_MODALITIES_IN_DB, false))
+    {
+      // Modalities are stored in the database
+      if (serverIndex_ == NULL)
+      {
+        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        Json::Value modalities;
+        SaveModalitiesToJson(modalities);
+
+        Json::FastWriter writer;
+        std::string s = writer.write(modalities);
+
+        serverIndex_->SetGlobalProperty(GlobalProperty_Modalities, s);
+      }
+    }
+    else
+    {
+      // Modalities are stored in the configuration files
+      if (!modalities_.empty() ||
+          json_.isMember(DICOM_MODALITIES))
+      {
+        SaveModalitiesToJson(json_[DICOM_MODALITIES]);
+      }
+    }
+  }
+
+
+  void OrthancConfiguration::SavePeers()
+  {
+    if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false))
+    {
+      // Peers are stored in the database
+      if (serverIndex_ == NULL)
+      {
+        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        Json::Value peers;
+        SavePeersToJson(peers);
+
+        Json::FastWriter writer;
+        std::string s = writer.write(peers);
+
+        serverIndex_->SetGlobalProperty(GlobalProperty_Peers, s);
+      }
+    }
+    else
+    {
+      // Peers are stored in the configuration files
+      if (!peers_.empty() ||
+          json_.isMember(ORTHANC_PEERS))
+      {
+        SavePeersToJson(json_[ORTHANC_PEERS]);
+      }
+    }
+  }
+
+
+  OrthancConfiguration& OrthancConfiguration::GetInstance()
+  {
+    static OrthancConfiguration configuration;
+    return configuration;
+  }
+
+
+  std::string OrthancConfiguration::GetStringParameter(const std::string& parameter,
+                                                       const std::string& defaultValue) const
+  {
+    if (json_.isMember(parameter))
+    {
+      if (json_[parameter].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadParameterType,
+                               "The configuration option \"" + parameter + "\" must be a string");
+      }
+      else
+      {
+        return json_[parameter].asString();
+      }
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+    
+  int OrthancConfiguration::GetIntegerParameter(const std::string& parameter,
+                                                int defaultValue) const
+  {
+    if (json_.isMember(parameter))
+    {
+      if (json_[parameter].type() != Json::intValue)
+      {
+        throw OrthancException(ErrorCode_BadParameterType,
+                               "The configuration option \"" + parameter + "\" must be an integer");
+      }
+      else
+      {
+        return json_[parameter].asInt();
+      }
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+    
+  unsigned int OrthancConfiguration::GetUnsignedIntegerParameter(
+    const std::string& parameter,
+    unsigned int defaultValue) const
+  {
+    int v = GetIntegerParameter(parameter, defaultValue);
+
+    if (v < 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "The configuration option \"" + parameter + "\" must be a positive integer");
+    }
+    else
+    {
+      return static_cast<unsigned int>(v);
+    }
+  }
+
+
+  bool OrthancConfiguration::GetBooleanParameter(const std::string& parameter,
+                                                 bool defaultValue) const
+  {
+    if (json_.isMember(parameter))
+    {
+      if (json_[parameter].type() != Json::booleanValue)
+      {
+        throw OrthancException(ErrorCode_BadParameterType,
+                               "The configuration option \"" + parameter +
+                               "\" must be a Boolean (true or false)");
+      }
+      else
+      {
+        return json_[parameter].asBool();
+      }
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  void OrthancConfiguration::Read(const char* configurationFile)
+  {
+    // Read the content of the configuration
+    configurationFileArg_ = configurationFile;
+    ReadConfiguration(json_, configurationFile);
+
+    // Adapt the paths to the configurations
+    defaultDirectory_ = boost::filesystem::current_path();
+    configurationAbsolutePath_ = "";
+
+    if (configurationFile)
+    {
+      if (boost::filesystem::is_directory(configurationFile))
+      {
+        defaultDirectory_ = boost::filesystem::path(configurationFile);
+        configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).parent_path().string();
+      }
+      else
+      {
+        defaultDirectory_ = boost::filesystem::path(configurationFile).parent_path();
+        configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).string();
+      }
+    }
+    else
+    {
+#if ORTHANC_STANDALONE != 1
+      // In a non-standalone build, we use the
+      // "Resources/Configuration.json" from the Orthanc source code
+
+      boost::filesystem::path p = ORTHANC_PATH;
+      p /= "Resources";
+      p /= "Configuration.json";
+      configurationAbsolutePath_ = boost::filesystem::absolute(p).string();
+#endif
+    }
+  }
+
+
+  void OrthancConfiguration::LoadModalitiesAndPeers()
+  {
+    LoadModalities();
+    LoadPeers();
+  }
+
+
+  void OrthancConfiguration::GetDicomModalityUsingSymbolicName(
+    RemoteModalityParameters& modality,
+    const std::string& name) const
+  {
+    Modalities::const_iterator found = modalities_.find(name);
+
+    if (found == modalities_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem,
+                             "No modality with symbolic name: " + name);
+    }
+    else
+    {
+      modality = found->second;
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupOrthancPeer(WebServiceParameters& peer,
+                                               const std::string& name) const
+  {
+    Peers::const_iterator found = peers_.find(name);
+
+    if (found == peers_.end())
+    {
+      LOG(ERROR) << "No peer with symbolic name: " << name;
+      return false;
+    }
+    else
+    {
+      peer = found->second;
+      return true;
+    }
+  }
+
+
+  void OrthancConfiguration::GetListOfDicomModalities(std::set<std::string>& target) const
+  {
+    target.clear();
+
+    for (Modalities::const_iterator 
+           it = modalities_.begin(); it != modalities_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  void OrthancConfiguration::GetListOfOrthancPeers(std::set<std::string>& target) const
+  {
+    target.clear();
+
+    for (Peers::const_iterator it = peers_.begin(); it != peers_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  void OrthancConfiguration::SetupRegisteredUsers(MongooseServer& httpServer) const
+  {
+    httpServer.ClearUsers();
+
+    if (!json_.isMember("RegisteredUsers"))
+    {
+      return;
+    }
+
+    const Json::Value& users = json_["RegisteredUsers"];
+    if (users.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users");
+    }
+
+    Json::Value::Members usernames = users.getMemberNames();
+    for (size_t i = 0; i < usernames.size(); i++)
+    {
+      const std::string& username = usernames[i];
+      std::string password = users[username].asString();
+      httpServer.RegisterUser(username.c_str(), password.c_str());
+    }
+  }
+    
+
+  std::string OrthancConfiguration::InterpretStringParameterAsPath(
+    const std::string& parameter) const
+  {
+    return SystemToolbox::InterpretRelativePath(defaultDirectory_.string(), parameter);
+  }
+
+    
+  void OrthancConfiguration::GetListOfStringsParameter(std::list<std::string>& target,
+                                                       const std::string& key) const
+  {
+    target.clear();
+  
+    if (!json_.isMember(key))
+    {
+      return;
+    }
+
+    const Json::Value& lst = json_[key];
+
+    if (lst.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of strings");
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < lst.size(); i++)
+    {
+      target.push_back(lst[i].asString());
+    }    
+  }
+
+    
+  bool OrthancConfiguration::IsSameAETitle(const std::string& aet1,
+                                           const std::string& aet2) const
+  {
+    if (GetBooleanParameter("StrictAetComparison", false))
+    {
+      // Case-sensitive matching
+      return aet1 == aet2;
+    }
+    else
+    {
+      // Case-insensitive matching (default)
+      std::string tmp1, tmp2;
+      Toolbox::ToLowerCase(tmp1, aet1);
+      Toolbox::ToLowerCase(tmp2, aet2);
+      return tmp1 == tmp2;
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality,
+                                                             const std::string& aet) const
+  {
+    for (Modalities::const_iterator it = modalities_.begin(); it != modalities_.end(); ++it)
+    {
+      if (IsSameAETitle(aet, it->second.GetApplicationEntityTitle()))
+      {
+        modality = it->second;
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  bool OrthancConfiguration::IsKnownAETitle(const std::string& aet,
+                                            const std::string& ip) const
+  {
+    RemoteModalityParameters modality;
+    
+    if (!LookupDicomModalityUsingAETitle(modality, aet))
+    {
+      LOG(WARNING) << "Modality \"" << aet
+                   << "\" is not listed in the \"DicomModalities\" configuration option";
+      return false;
+    }
+    else if (!GetBooleanParameter("DicomCheckModalityHost", false) ||
+             ip == modality.GetHost())
+    {
+      return true;
+    }
+    else
+    {
+      LOG(WARNING) << "Forbidding access from AET \"" << aet
+                   << "\" given its hostname (" << ip << ") does not match "
+                   << "the \"DicomModalities\" configuration option ("
+                   << modality.GetHost() << " was expected)";
+      return false;
+    }
+  }
+
+
+  RemoteModalityParameters 
+  OrthancConfiguration::GetModalityUsingSymbolicName(const std::string& name) const
+  {
+    RemoteModalityParameters modality;
+    GetDicomModalityUsingSymbolicName(modality, name);
+
+    return modality;
+  }
+
+    
+  RemoteModalityParameters 
+  OrthancConfiguration::GetModalityUsingAet(const std::string& aet) const
+  {
+    RemoteModalityParameters modality;
+      
+    if (LookupDicomModalityUsingAETitle(modality, aet))
+    {
+      return modality;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentItem,
+                             "Unknown modality for AET: " + aet);
+    }
+  }
+
+    
+  void OrthancConfiguration::UpdateModality(const std::string& symbolicName,
+                                            const RemoteModalityParameters& modality)
+  {
+    modalities_[symbolicName] = modality;
+    SaveModalities();
+  }
+
+
+  void OrthancConfiguration::RemoveModality(const std::string& symbolicName)
+  {
+    modalities_.erase(symbolicName);
+    SaveModalities();
+  }
+
+    
+  void OrthancConfiguration::UpdatePeer(const std::string& symbolicName,
+                                        const WebServiceParameters& peer)
+  {
+    peer.CheckClientCertificate();
+
+    peers_[symbolicName] = peer;
+    SavePeers();
+  }
+
+
+  void OrthancConfiguration::RemovePeer(const std::string& symbolicName)
+  {
+    peers_.erase(symbolicName);
+    SavePeers();
+  }
+
+
+  void OrthancConfiguration::Format(std::string& result) const
+  {
+    Json::StyledWriter w;
+    result = w.write(json_);
+  }
+
+
+  void OrthancConfiguration::SetDefaultEncoding(Encoding encoding)
+  {
+    SetDefaultDicomEncoding(encoding);
+
+    // Propagate the encoding to the configuration file that is
+    // stored in memory
+    json_["DefaultEncoding"] = EnumerationToString(encoding);
+  }
+
+
+  bool OrthancConfiguration::HasConfigurationChanged() const
+  {
+    Json::Value current;
+    ReadConfiguration(current, configurationFileArg_);
+
+    Json::FastWriter writer;
+    std::string a = writer.write(json_);
+    std::string b = writer.write(current);
+
+    return a != b;
+  }
+
+
+  void OrthancConfiguration::SetServerIndex(ServerIndex& index)
+  {
+    serverIndex_ = &index;
+  }
+
+
+  void OrthancConfiguration::ResetServerIndex()
+  {
+    serverIndex_ = NULL;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancConfiguration.h	Thu Dec 06 15:58:08 2018 +0100
@@ -0,0 +1,228 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Core/Images/FontRegistry.h"
+#include "../Core/WebServiceParameters.h"
+#include "../Core/DicomNetworking/RemoteModalityParameters.h"
+
+#include <EmbeddedResources.h>
+
+#include <boost/filesystem.hpp>
+#include <boost/thread/shared_mutex.hpp>
+#include <boost/thread/lock_types.hpp>
+
+namespace Orthanc
+{
+  class MongooseServer;
+  class ServerIndex;
+  
+  class OrthancConfiguration : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, RemoteModalityParameters>   Modalities;
+    typedef std::map<std::string, WebServiceParameters>       Peers;
+
+    boost::shared_mutex      mutex_;
+    Json::Value              json_;
+    boost::filesystem::path  defaultDirectory_;
+    std::string              configurationAbsolutePath_;
+    FontRegistry             fontRegistry_;
+    const char*              configurationFileArg_;
+    Modalities               modalities_;
+    Peers                    peers_;
+    ServerIndex*             serverIndex_;
+
+    OrthancConfiguration() :
+      configurationFileArg_(NULL)
+    {
+    }
+
+    void LoadModalitiesFromJson(const Json::Value& source);
+    
+    void LoadPeersFromJson(const Json::Value& source);
+    
+    void LoadModalities();
+    
+    void LoadPeers();
+    
+    void SaveModalitiesToJson(Json::Value& target);
+    
+    void SavePeersToJson(Json::Value& target);
+    
+    void SaveModalities();
+    
+    void SavePeers();
+
+    static OrthancConfiguration& GetInstance();
+
+  public:
+    class ReaderLock : public boost::noncopyable
+    {
+    private:
+      OrthancConfiguration&                    configuration_;
+      boost::shared_lock<boost::shared_mutex>  lock_;
+
+    public:
+      ReaderLock() :
+        configuration_(GetInstance()),
+        lock_(configuration_.mutex_)
+      {
+      }
+
+      const OrthancConfiguration& GetConfiguration() const
+      {
+        return configuration_;
+      }
+
+      const Json::Value& GetJson() const
+      {
+        return configuration_.json_;
+      }
+    };
+
+
+    class WriterLock : public boost::noncopyable
+    {
+    private:
+      OrthancConfiguration&                    configuration_;
+      boost::unique_lock<boost::shared_mutex>  lock_;
+
+    public:
+      WriterLock() :
+        configuration_(GetInstance()),
+        lock_(configuration_.mutex_)
+      {
+      }
+
+      OrthancConfiguration& GetConfiguration()
+      {
+        return configuration_;
+      }
+
+      const OrthancConfiguration& GetConfiguration() const
+      {
+        return configuration_;
+      }
+
+      const Json::Value& GetJson() const
+      {
+        return configuration_.json_;
+      }
+    };
+
+
+    const std::string& GetConfigurationAbsolutePath() const
+    {
+      return configurationAbsolutePath_;
+    }
+
+    const FontRegistry& GetFontRegistry() const
+    {
+      return fontRegistry_;
+    }
+
+    void Read(const char* configurationFile);
+
+    void LoadModalitiesAndPeers();
+    
+    void RegisterFont(EmbeddedResources::FileResourceId resource)
+    {
+      fontRegistry_.AddFromResource(resource);
+    }
+
+    std::string GetStringParameter(const std::string& parameter,
+                                   const std::string& defaultValue) const;
+    
+    int GetIntegerParameter(const std::string& parameter,
+                            int defaultValue) const;
+    
+    unsigned int GetUnsignedIntegerParameter(const std::string& parameter,
+                                             unsigned int defaultValue) const;
+
+    bool GetBooleanParameter(const std::string& parameter,
+                             bool defaultValue) const;
+
+    void GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality,
+                                           const std::string& name) const;
+
+    bool LookupOrthancPeer(WebServiceParameters& peer,
+                           const std::string& name) const;
+
+    void GetListOfDicomModalities(std::set<std::string>& target) const;
+
+    void GetListOfOrthancPeers(std::set<std::string>& target) const;
+
+    void SetupRegisteredUsers(MongooseServer& httpServer) const;
+
+    std::string InterpretStringParameterAsPath(const std::string& parameter) const;
+    
+    void GetListOfStringsParameter(std::list<std::string>& target,
+                                   const std::string& key) const;
+    
+    bool IsSameAETitle(const std::string& aet1,
+                       const std::string& aet2) const;
+
+    bool LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality,
+                                         const std::string& aet) const;
+
+    bool IsKnownAETitle(const std::string& aet,
+                        const std::string& ip) const;
+
+    RemoteModalityParameters GetModalityUsingSymbolicName(const std::string& name) const;
+    
+    RemoteModalityParameters GetModalityUsingAet(const std::string& aet) const;
+    
+    void UpdateModality(const std::string& symbolicName,
+                        const RemoteModalityParameters& modality);
+
+    void RemoveModality(const std::string& symbolicName);
+    
+    void UpdatePeer(const std::string& symbolicName,
+                    const WebServiceParameters& peer);
+
+    void RemovePeer(const std::string& symbolicName);
+
+
+    void Format(std::string& result) const;
+    
+    void SetDefaultEncoding(Encoding encoding);
+
+    bool HasConfigurationChanged() const;
+
+    void SetServerIndex(ServerIndex& index);
+
+    void ResetServerIndex();
+  };
+}
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -38,7 +38,7 @@
 #include "../Core/Lua/LuaFunctionCall.h"
 #include "../Core/Logging.h"
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "OrthancInitialization.h"
+#include "OrthancConfiguration.h"
 #include "Search/LookupResource.h"
 #include "ServerToolbox.h"
 
@@ -258,16 +258,21 @@
         // The metadata "SopClassUid" is available for each of these instances
         StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
       }
-      else if (Configuration::GetGlobalBoolParameter("AllowFindSopClassesInStudy", false))
-      {
-        ExtractTagFromInstancesOnDisk(values, context, DICOM_TAG_SOP_CLASS_UID, instances);
-        StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
-      }
       else
       {
-        result.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, "", false);
-        LOG(WARNING) << "The handling of \"SOP Classes in Study\" (0008,0062) "
-                     << "in C-FIND requests is disabled";
+        OrthancConfiguration::ReaderLock lock;
+
+        if (lock.GetConfiguration().GetBooleanParameter("AllowFindSopClassesInStudy", false))
+        {
+          ExtractTagFromInstancesOnDisk(values, context, DICOM_TAG_SOP_CLASS_UID, instances);
+          StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
+        }
+        else
+        {
+          result.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, "", false);
+          LOG(WARNING) << "The handling of \"SOP Classes in Study\" (0008,0062) "
+                       << "in C-FIND requests is disabled";
+        }
       }
     }
   }
@@ -547,8 +552,8 @@
         levelTmp->IsNull() ||
         levelTmp->IsBinary())
     {
-      LOG(ERROR) << "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)";
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest,
+                             "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)");
     }
 
     ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());
@@ -590,7 +595,12 @@
 
     LookupResource lookup(level);
 
-    const bool caseSensitivePN = Configuration::GetGlobalBoolParameter("CaseSensitivePN", false);
+    bool caseSensitivePN;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false);
+    }
 
     for (size_t i = 0; i < query.GetSize(); i++)
     {
--- a/OrthancServer/OrthancInitialization.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancInitialization.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -39,22 +39,14 @@
 #endif
 
 #include "OrthancInitialization.h"
-#include "ServerContext.h"
 
-#include "../Core/HttpClient.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/FileStorage/FilesystemStorage.h"
 #include "../Core/Logging.h"
 #include "../Core/OrthancException.h"
-#include "../Core/Toolbox.h"
-#include "../Core/FileStorage/FilesystemStorage.h"
 
-#include "ServerEnumerations.h"
 #include "DatabaseWrapper.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-
-#include <boost/lexical_cast.hpp>
-#include <boost/filesystem.hpp>
-#include <curl/curl.h>
-#include <boost/thread/recursive_mutex.hpp>
+#include "OrthancConfiguration.h"
 
 #include <dcmtk/dcmnet/dul.h>   // For dcmDisableGethostbyaddr()
 
@@ -62,239 +54,11 @@
 
 namespace Orthanc
 {
-  static boost::recursive_mutex globalMutex_;
-  static Json::Value configuration_;
-  static boost::filesystem::path defaultDirectory_;
-  static std::string configurationAbsolutePath_;
-  static FontRegistry fontRegistry_;
-  static const char* configurationFileArg_ = NULL;
-
-
-  static std::string GetGlobalStringParameterInternal(const std::string& parameter,
-                                                      const std::string& defaultValue)
-  {
-    if (configuration_.isMember(parameter))
-    {
-      if (configuration_[parameter].type() != Json::stringValue)
-      {
-        LOG(ERROR) << "The configuration option \"" << parameter << "\" must be a string";
-        throw OrthancException(ErrorCode_BadParameterType);
-      }
-      else
-      {
-        return configuration_[parameter].asString();
-      }
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-
-  static bool GetGlobalBoolParameterInternal(const std::string& parameter,
-                                             bool defaultValue)
+  static void RegisterUserMetadata(const Json::Value& config)
   {
-    if (configuration_.isMember(parameter))
-    {
-      if (configuration_[parameter].type() != Json::booleanValue)
-      {
-        LOG(ERROR) << "The configuration option \"" << parameter << "\" must be a Boolean (true or false)";
-        throw OrthancException(ErrorCode_BadParameterType);
-      }
-      else
-      {
-        return configuration_[parameter].asBool();
-      }
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-
-
-  static void AddFileToConfiguration(Json::Value& target,
-                                     const boost::filesystem::path& path)
-  {
-    LOG(WARNING) << "Reading the configuration from: " << path;
-
-    Json::Value config;
-
-    {
-      std::string content;
-      SystemToolbox::ReadFile(content, path.string());
-
-      Json::Value tmp;
-      Json::Reader reader;
-      if (!reader.parse(content, tmp) ||
-          tmp.type() != Json::objectValue)
-      {
-        LOG(ERROR) << "The configuration file does not follow the JSON syntax: " << path;
-        throw OrthancException(ErrorCode_BadJson);
-      }
-
-      Toolbox::CopyJsonWithoutComments(config, tmp);
-    }
-
-    if (target.size() == 0)
-    {
-      target = config;
-    }
-    else
-    {
-      Json::Value::Members members = config.getMemberNames();
-      for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
-      {
-        if (target.isMember(members[i]))
-        {
-          LOG(ERROR) << "The configuration section \"" << members[i] << "\" is defined in 2 different configuration files";
-          throw OrthancException(ErrorCode_BadFileFormat);          
-        }
-        else
-        {
-          target[members[i]] = config[members[i]];
-        }
-      }
-    }
-  }
-
-
-  static void ScanFolderForConfiguration(Json::Value& target,
-                                         const char* folder)
-  {
-    using namespace boost::filesystem;
-
-    LOG(WARNING) << "Scanning folder \"" << folder << "\" for configuration files";
-
-    directory_iterator end_it; // default construction yields past-the-end
-    for (directory_iterator it(folder);
-         it != end_it;
-         ++it)
+    if (config.isMember("UserMetadata"))
     {
-      if (!is_directory(it->status()))
-      {
-        std::string extension = boost::filesystem::extension(it->path());
-        Toolbox::ToLowerCase(extension);
-
-        if (extension == ".json")
-        {
-          AddFileToConfiguration(target, it->path().string());
-        }
-      }
-    }
-  }
-
-
-  static void ReadConfiguration(Json::Value& target,
-                                const char* configurationFile)
-  {
-    target = Json::objectValue;
-
-    if (configurationFile)
-    {
-      if (!boost::filesystem::exists(configurationFile))
-      {
-        LOG(ERROR) << "Inexistent path to configuration: " << configurationFile;
-        throw OrthancException(ErrorCode_InexistentFile);
-      }
-      
-      if (boost::filesystem::is_directory(configurationFile))
-      {
-        ScanFolderForConfiguration(target, configurationFile);
-      }
-      else
-      {
-        AddFileToConfiguration(target, configurationFile);
-      }
-    }
-    else
-    {
-#if ORTHANC_STANDALONE == 1
-      // No default path for the standalone configuration
-      LOG(WARNING) << "Using the default Orthanc configuration";
-      return;
-
-#else
-      // In a non-standalone build, we use the
-      // "Resources/Configuration.json" from the Orthanc source code
-
-      boost::filesystem::path p = ORTHANC_PATH;
-      p /= "Resources";
-      p /= "Configuration.json";
-
-      AddFileToConfiguration(target, p);
-#endif
-    }
-  }
-
-
-
-  static void ReadGlobalConfiguration(const char* configurationFile)
-  {
-    // Read the content of the configuration
-    configurationFileArg_ = configurationFile;
-    ReadConfiguration(configuration_, configurationFile);
-
-    // Adapt the paths to the configurations
-    defaultDirectory_ = boost::filesystem::current_path();
-    configurationAbsolutePath_ = "";
-
-    if (configurationFile)
-    {
-      if (boost::filesystem::is_directory(configurationFile))
-      {
-        defaultDirectory_ = boost::filesystem::path(configurationFile);
-        configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).parent_path().string();
-      }
-      else
-      {
-        defaultDirectory_ = boost::filesystem::path(configurationFile).parent_path();
-        configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).string();
-      }
-    }
-    else
-    {
-#if ORTHANC_STANDALONE != 1
-      // In a non-standalone build, we use the
-      // "Resources/Configuration.json" from the Orthanc source code
-
-      boost::filesystem::path p = ORTHANC_PATH;
-      p /= "Resources";
-      p /= "Configuration.json";
-      configurationAbsolutePath_ = boost::filesystem::absolute(p).string();
-#endif
-    }
-  }
-
-
-  static void ValidateGlobalConfiguration()
-  {
-    std::set<std::string> ids;
-
-    Configuration::GetListOfOrthancPeers(ids);
-    for (std::set<std::string>::const_iterator it = ids.begin(); it != ids.end(); ++it)
-    {
-      WebServiceParameters peer;
-      Configuration::GetOrthancPeer(peer, *it);
-      peer.CheckClientCertificate();
-    }
-
-    Configuration::GetListOfDicomModalities(ids);
-    for (std::set<std::string>::const_iterator it = ids.begin(); it != ids.end(); ++it)
-    {
-      RemoteModalityParameters modality;
-      Configuration::GetDicomModalityUsingSymbolicName(modality, *it);
-    }
-  }
-
-
-  static void RegisterUserMetadata()
-  {
-    if (configuration_.isMember("UserMetadata"))
-    {
-      const Json::Value& parameter = configuration_["UserMetadata"];
+      const Json::Value& parameter = config["UserMetadata"];
 
       Json::Value::Members members = parameter.getMemberNames();
       for (size_t i = 0; i < members.size(); i++)
@@ -303,8 +67,8 @@
 
         if (!parameter[name].isInt())
         {
-          LOG(ERROR) << "Not a number in this user-defined metadata: " << name;
-          throw OrthancException(ErrorCode_BadParameterType);
+          throw OrthancException(ErrorCode_BadParameterType,
+                                 "Not a number in this user-defined metadata: " + name);
         }
 
         int metadata = parameter[name].asInt();        
@@ -326,17 +90,17 @@
   }
 
 
-  static void RegisterUserContentType()
+  static void RegisterUserContentType(const Json::Value& config)
   {
-    if (configuration_.isMember("UserContentType"))
+    if (config.isMember("UserContentType"))
     {
-      const Json::Value& parameter = configuration_["UserContentType"];
+      const Json::Value& parameter = config["UserContentType"];
 
       Json::Value::Members members = parameter.getMemberNames();
       for (size_t i = 0; i < members.size(); i++)
       {
         const std::string& name = members[i];
-        std::string mime = "application/octet-stream";
+        std::string mime = MIME_BINARY;
 
         const Json::Value& value = parameter[name];
         int contentType;
@@ -355,8 +119,8 @@
         }
         else
         {
-          LOG(ERROR) << "Not a number in this user-defined attachment type: " << name;
-          throw OrthancException(ErrorCode_BadParameterType);
+          throw OrthancException(ErrorCode_BadParameterType,
+                                 "Not a number in this user-defined attachment type: " + name);
         }
 
         LOG(INFO) << "Registering user-defined attachment type: " << name << " (index " 
@@ -419,8 +183,9 @@
         !config.isMember("Module") ||
         config["Module"].type() != Json::stringValue)
     {
-      LOG(ERROR) << "No path to the PKCS#11 module (DLL or .so) is provided for HTTPS client authentication";
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "No path to the PKCS#11 module (DLL or .so) is provided "
+                             "for HTTPS client authentication");
     }
 
     std::string pin;
@@ -432,8 +197,8 @@
       }
       else
       {
-        LOG(ERROR) << "The PIN number in the PKCS#11 configuration must be a string";
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "The PIN number in the PKCS#11 configuration must be a string");
       }
     }
 
@@ -446,8 +211,8 @@
       }
       else
       {
-        LOG(ERROR) << "The Verbose option in the PKCS#11 configuration must be a Boolean";
-        throw OrthancException(ErrorCode_BadFileFormat);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "The Verbose option in the PKCS#11 configuration must be a Boolean");
       }
     }
 
@@ -458,29 +223,28 @@
 
   void OrthancInitialize(const char* configurationFile)
   {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
+    OrthancConfiguration::WriterLock lock;
 
     Toolbox::InitializeOpenSsl();
 
     InitializeServerEnumerations();
 
     // Read the user-provided configuration
-    ReadGlobalConfiguration(configurationFile);
-    ValidateGlobalConfiguration();
+    lock.GetConfiguration().Read(configurationFile);
 
-    if (configuration_.isMember("Locale"))
+    if (lock.GetJson().isMember("Locale"))
     {
-      std::string locale = GetGlobalStringParameterInternal("Locale", "");
-      Toolbox::InitializeGlobalLocale(configuration_["Locale"].asCString());
+      std::string locale = lock.GetConfiguration().GetStringParameter("Locale", "");
+      Toolbox::InitializeGlobalLocale(lock.GetJson()["Locale"].asCString());
     }
     else
     {
       Toolbox::InitializeGlobalLocale(NULL);
     }
 
-    if (configuration_.isMember("DefaultEncoding"))
+    if (lock.GetJson().isMember("DefaultEncoding"))
     {
-      std::string encoding = GetGlobalStringParameterInternal("DefaultEncoding", "");
+      std::string encoding = lock.GetConfiguration().GetStringParameter("DefaultEncoding", "");
       SetDefaultDicomEncoding(StringToEncoding(encoding.c_str()));
     }
     else
@@ -488,22 +252,23 @@
       SetDefaultDicomEncoding(ORTHANC_DEFAULT_DICOM_ENCODING);
     }
 
-    if (configuration_.isMember("Pkcs11"))
+    if (lock.GetJson().isMember("Pkcs11"))
     {
-      ConfigurePkcs11(configuration_["Pkcs11"]);
+      ConfigurePkcs11(lock.GetJson()["Pkcs11"]);
     }
 
     HttpClient::GlobalInitialize();
 
-    RegisterUserMetadata();
-    RegisterUserContentType();
+    RegisterUserMetadata(lock.GetJson());
+    RegisterUserContentType(lock.GetJson());
 
-    FromDcmtkBridge::InitializeDictionary(GetGlobalBoolParameterInternal("LoadPrivateDictionary", true));
-    LoadCustomDictionary(configuration_);
+    bool loadPrivate = lock.GetConfiguration().GetBooleanParameter("LoadPrivateDictionary", true);
+    FromDcmtkBridge::InitializeDictionary(loadPrivate);
+    LoadCustomDictionary(lock.GetJson());
 
     FromDcmtkBridge::InitializeCodecs();
 
-    fontRegistry_.AddFromResource(EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
+    lock.GetConfiguration().RegisterFont(EmbeddedResources::FONT_UBUNTU_MONO_BOLD_16);
 
     /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */
     dcmDisableGethostbyaddr.set(OFTrue);
@@ -513,7 +278,8 @@
 
   void OrthancFinalize()
   {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
+    OrthancConfiguration::WriterLock lock;
+
     HttpClient::GlobalFinalize();
     FromDcmtkBridge::FinalizeCodecs();
     Toolbox::FinalizeOpenSsl();
@@ -521,531 +287,16 @@
   }
 
 
-  std::string Configuration::GetGlobalStringParameter(const std::string& parameter,
-                                                      const std::string& defaultValue)
-  {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
-    return GetGlobalStringParameterInternal(parameter, defaultValue);
-  }
-
-
-  int Configuration::GetGlobalIntegerParameter(const std::string& parameter,
-                                               int defaultValue)
-  {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
-
-    if (configuration_.isMember(parameter))
-    {
-      if (configuration_[parameter].type() != Json::intValue)
-      {
-        LOG(ERROR) << "The configuration option \"" << parameter << "\" must be an integer";
-        throw OrthancException(ErrorCode_BadParameterType);
-      }
-      else
-      {
-        return configuration_[parameter].asInt();
-      }
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-
-  unsigned int Configuration::GetGlobalUnsignedIntegerParameter(const std::string& parameter,
-                                                                unsigned int defaultValue)
-  {
-    int v = GetGlobalIntegerParameter(parameter, defaultValue);
-
-    if (v < 0)
-    {
-      LOG(ERROR) << "The configuration option \"" << parameter << "\" must be a positive integer";
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      return static_cast<unsigned int>(v);
-    }
-  }
-
-
-  bool Configuration::GetGlobalBoolParameter(const std::string& parameter,
-                                             bool defaultValue)
-  {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
-    return GetGlobalBoolParameterInternal(parameter, defaultValue);
-  }
-
-
-  void Configuration::GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality,
-                                                        const std::string& name)
-  {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
-
-    if (!configuration_.isMember("DicomModalities"))
-    {
-      LOG(ERROR) << "No modality with symbolic name: " << name;
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-
-    const Json::Value& modalities = configuration_["DicomModalities"];
-    if (modalities.type() != Json::objectValue ||
-        !modalities.isMember(name))
-    {
-      LOG(ERROR) << "No modality with symbolic name: " << name;
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-
-    try
-    {
-      modality.Unserialize(modalities[name]);
-    }
-    catch (OrthancException&)
-    {
-      LOG(ERROR) << "Syntax error in the definition of DICOM modality \"" << name 
-                 << "\". Please check your configuration file.";
-      throw;
-    }
-  }
-
-
-  bool Configuration::GetOrthancPeer(WebServiceParameters& peer,
-                                     const std::string& name)
-  {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
-
-    if (!configuration_.isMember("OrthancPeers"))
-    {
-      return false;
-    }
-
-    try
-    {
-      const Json::Value& modalities = configuration_["OrthancPeers"];
-      if (modalities.type() != Json::objectValue ||
-          !modalities.isMember(name))
-      {
-        return false;
-      }
-      else
-      {
-        peer.Unserialize(modalities[name]);
-        return true;
-      }
-    }
-    catch (OrthancException&)
-    {
-      LOG(ERROR) << "Syntax error in the definition of peer \"" << name 
-                 << "\". Please check your configuration file.";
-      throw;
-    }
-  }
-
-
-  static bool ReadKeys(std::set<std::string>& target,
-                       const char* parameter,
-                       bool onlyAlphanumeric)
-  {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
-
-    target.clear();
-  
-    if (!configuration_.isMember(parameter))
-    {
-      return true;
-    }
-
-    const Json::Value& modalities = configuration_[parameter];
-    if (modalities.type() != Json::objectValue)
-    {
-      LOG(ERROR) << "Bad format of the \"DicomModalities\" configuration section";
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    Json::Value::Members members = modalities.getMemberNames();
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      if (onlyAlphanumeric)
-      {
-        for (size_t j = 0; j < members[i].size(); j++)
-        {
-          if (!isalnum(members[i][j]) && members[i][j] != '-')
-          {
-            return false;
-          }
-        }
-      }
-
-      target.insert(members[i]);
-    }
-
-    return true;
-  }
-
-
-  void Configuration::GetListOfDicomModalities(std::set<std::string>& target)
-  {
-    target.clear();
-
-    if (!ReadKeys(target, "DicomModalities", true))
-    {
-      LOG(ERROR) << "Only alphanumeric and dash characters are allowed in the names of the modalities";
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-
-  void Configuration::GetListOfOrthancPeers(std::set<std::string>& target)
-  {
-    target.clear();
-
-    if (!ReadKeys(target, "OrthancPeers", true))
-    {
-      LOG(ERROR) << "Only alphanumeric and dash characters are allowed in the names of Orthanc peers";
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-
-
-  void Configuration::SetupRegisteredUsers(MongooseServer& httpServer)
-  {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
-
-    httpServer.ClearUsers();
-
-    if (!configuration_.isMember("RegisteredUsers"))
-    {
-      return;
-    }
-
-    const Json::Value& users = configuration_["RegisteredUsers"];
-    if (users.type() != Json::objectValue)
-    {
-      LOG(ERROR) << "Badly formatted list of users";
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    Json::Value::Members usernames = users.getMemberNames();
-    for (size_t i = 0; i < usernames.size(); i++)
-    {
-      const std::string& username = usernames[i];
-      std::string password = users[username].asString();
-      httpServer.RegisterUser(username.c_str(), password.c_str());
-    }
-  }
-
-
-  std::string Configuration::InterpretRelativePath(const std::string& baseDirectory,
-                                                   const std::string& relativePath)
-  {
-    boost::filesystem::path base(baseDirectory);
-    boost::filesystem::path relative(relativePath);
-
-    /**
-       The following lines should be equivalent to this one: 
-
-       return (base / relative).string();
-
-       However, for some unknown reason, some versions of Boost do not
-       make the proper path resolution when "baseDirectory" is an
-       absolute path. So, a hack is used below.
-    **/
-
-    if (relative.is_absolute())
-    {
-      return relative.string();
-    }
-    else
-    {
-      return (base / relative).string();
-    }
-  }
-
-  std::string Configuration::InterpretStringParameterAsPath(const std::string& parameter)
-  {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
-    return InterpretRelativePath(defaultDirectory_.string(), parameter);
-  }
-
-
-  void Configuration::GetGlobalListOfStringsParameter(std::list<std::string>& target,
-                                                      const std::string& key)
-  {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
-
-    target.clear();
-  
-    if (!configuration_.isMember(key))
-    {
-      return;
-    }
-
-    const Json::Value& lst = configuration_[key];
-
-    if (lst.type() != Json::arrayValue)
-    {
-      LOG(ERROR) << "Badly formatted list of strings";
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    for (Json::Value::ArrayIndex i = 0; i < lst.size(); i++)
-    {
-      target.push_back(lst[i].asString());
-    }    
-  }
-
-
-  bool Configuration::IsSameAETitle(const std::string& aet1,
-                                    const std::string& aet2)
-  {
-    if (GetGlobalBoolParameter("StrictAetComparison", false))
-    {
-      // Case-sensitive matching
-      return aet1 == aet2;
-    }
-    else
-    {
-      // Case-insensitive matching (default)
-      std::string tmp1, tmp2;
-      Toolbox::ToLowerCase(tmp1, aet1);
-      Toolbox::ToLowerCase(tmp2, aet2);
-      return tmp1 == tmp2;
-    }
-  }
-
-
-  bool Configuration::LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality,
-                                                      const std::string& aet)
-  {
-    std::set<std::string> modalities;
-    GetListOfDicomModalities(modalities);
-
-    for (std::set<std::string>::const_iterator 
-           it = modalities.begin(); it != modalities.end(); ++it)
-    {
-      try
-      {
-        GetDicomModalityUsingSymbolicName(modality, *it);
-
-        if (IsSameAETitle(aet, modality.GetApplicationEntityTitle()))
-        {
-          return true;
-        }
-      }
-      catch (OrthancException&)
-      {
-      }
-    }
-
-    return false;
-  }
-
-
-  bool Configuration::IsKnownAETitle(const std::string& aet,
-                                     const std::string& ip)
-  {
-    RemoteModalityParameters modality;
-    
-    if (!LookupDicomModalityUsingAETitle(modality, aet))
-    {
-      LOG(WARNING) << "Modality \"" << aet
-                   << "\" is not listed in the \"DicomModalities\" configuration option";
-      return false;
-    }
-    else if (!Configuration::GetGlobalBoolParameter("DicomCheckModalityHost", false) ||
-             ip == modality.GetHost())
-    {
-      return true;
-    }
-    else
-    {
-      LOG(WARNING) << "Forbidding access from AET \"" << aet
-                   << "\" given its hostname (" << ip << ") does not match "
-                   << "the \"DicomModalities\" configuration option ("
-                   << modality.GetHost() << " was expected)";
-      return false;
-    }
-  }
-
-
-  RemoteModalityParameters Configuration::GetModalityUsingSymbolicName(const std::string& name)
-  {
-    RemoteModalityParameters modality;
-    GetDicomModalityUsingSymbolicName(modality, name);
-
-    return modality;
-  }
-
-
-  RemoteModalityParameters Configuration::GetModalityUsingAet(const std::string& aet)
-  {
-    RemoteModalityParameters modality;
-
-    if (LookupDicomModalityUsingAETitle(modality, aet))
-    {
-      return modality;
-    }
-    else
-    {
-      LOG(ERROR) << "Unknown modality for AET: " << aet;
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-  }
-
-
-  void Configuration::UpdateModality(ServerContext& context,
-                                     const std::string& symbolicName,
-                                     const RemoteModalityParameters& modality)
-  {
-    {
-      boost::recursive_mutex::scoped_lock lock(globalMutex_);
-
-      if (!configuration_.isMember("DicomModalities"))
-      {
-        configuration_["DicomModalities"] = Json::objectValue;
-      }
-
-      Json::Value& modalities = configuration_["DicomModalities"];
-      if (modalities.type() != Json::objectValue)
-      {
-        LOG(ERROR) << "Bad file format for modality: " << symbolicName;
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      modalities.removeMember(symbolicName);
-
-      Json::Value v;
-      modality.Serialize(v, true /* force advanced format */);
-      modalities[symbolicName] = v;
-    }
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (context.HasPlugins())
-    {
-      context.GetPlugins().SignalUpdatedModalities();
-    }
-#endif
-  }
-  
-
-  void Configuration::RemoveModality(ServerContext& context,
-                                     const std::string& symbolicName)
-  {
-    {
-      boost::recursive_mutex::scoped_lock lock(globalMutex_);
-
-      if (!configuration_.isMember("DicomModalities"))
-      {
-        LOG(ERROR) << "No modality with symbolic name: " << symbolicName;
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      Json::Value& modalities = configuration_["DicomModalities"];
-      if (modalities.type() != Json::objectValue)
-      {
-        LOG(ERROR) << "Bad file format for the \"DicomModalities\" configuration section";
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      modalities.removeMember(symbolicName.c_str());
-    }
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (context.HasPlugins())
-    {
-      context.GetPlugins().SignalUpdatedModalities();
-    }
-#endif
-  }
-
-
-  void Configuration::UpdatePeer(ServerContext& context,
-                                 const std::string& symbolicName,
-                                 const WebServiceParameters& peer)
-  {
-    peer.CheckClientCertificate();
-
-    {
-      boost::recursive_mutex::scoped_lock lock(globalMutex_);
-
-      if (!configuration_.isMember("OrthancPeers"))
-      {
-        LOG(ERROR) << "No peer with symbolic name: " << symbolicName;
-        configuration_["OrthancPeers"] = Json::objectValue;
-      }
-
-      Json::Value& peers = configuration_["OrthancPeers"];
-      if (peers.type() != Json::objectValue)
-      {
-        LOG(ERROR) << "Bad file format for the \"OrthancPeers\" configuration section";
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      peers.removeMember(symbolicName);
-
-      Json::Value v;
-      peer.Serialize(v, 
-                     false /* use simple format if possible */, 
-                     true  /* include passwords */);
-      peers[symbolicName] = v;
-    }
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (context.HasPlugins())
-    {
-      context.GetPlugins().SignalUpdatedPeers();
-    }
-#endif
-  }
-  
-
-  void Configuration::RemovePeer(ServerContext& context,
-                                 const std::string& symbolicName)
-  {
-    {
-      boost::recursive_mutex::scoped_lock lock(globalMutex_);
-
-      if (!configuration_.isMember("OrthancPeers"))
-      {
-        LOG(ERROR) << "No peer with symbolic name: " << symbolicName;
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      Json::Value& peers = configuration_["OrthancPeers"];
-      if (peers.type() != Json::objectValue)
-      {
-        LOG(ERROR) << "Bad file format for the \"OrthancPeers\" configuration section";
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      peers.removeMember(symbolicName.c_str());
-    }
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (context.HasPlugins())
-    {
-      context.GetPlugins().SignalUpdatedPeers();
-    }
-#endif
-  }
-
-
-  
-  const std::string& Configuration::GetConfigurationAbsolutePath()
-  {
-    return configurationAbsolutePath_;
-  }
-
-
   static IDatabaseWrapper* CreateSQLiteWrapper()
   {
-    std::string storageDirectoryStr = Configuration::GetGlobalStringParameter("StorageDirectory", "OrthancStorage");
+    OrthancConfiguration::ReaderLock lock;
+
+    std::string storageDirectoryStr = 
+      lock.GetConfiguration().GetStringParameter("StorageDirectory", "OrthancStorage");
 
     // Open the database
-    boost::filesystem::path indexDirectory = Configuration::InterpretStringParameterAsPath(
-      Configuration::GetGlobalStringParameter("IndexDirectory", storageDirectoryStr));
+    boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath(
+      lock.GetConfiguration().GetStringParameter("IndexDirectory", storageDirectoryStr));
 
     LOG(WARNING) << "SQLite index directory: " << indexDirectory;
 
@@ -1114,12 +365,17 @@
 
   static IStorageArea* CreateFilesystemStorage()
   {
-    std::string storageDirectoryStr = Configuration::GetGlobalStringParameter("StorageDirectory", "OrthancStorage");
+    OrthancConfiguration::ReaderLock lock;
 
-    boost::filesystem::path storageDirectory = Configuration::InterpretStringParameterAsPath(storageDirectoryStr);
+    std::string storageDirectoryStr = 
+      lock.GetConfiguration().GetStringParameter("StorageDirectory", "OrthancStorage");
+
+    boost::filesystem::path storageDirectory = 
+      lock.GetConfiguration().InterpretStringParameterAsPath(storageDirectoryStr);
+
     LOG(WARNING) << "Storage directory: " << storageDirectory;
 
-    if (Configuration::GetGlobalBoolParameter("StoreDicom", true))
+    if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true))
     {
       return new FilesystemStorage(storageDirectory.string());
     }
@@ -1131,66 +387,14 @@
   }
 
 
-  IDatabaseWrapper* Configuration::CreateDatabaseWrapper()
+  IDatabaseWrapper* CreateDatabaseWrapper()
   {
     return CreateSQLiteWrapper();
   }
 
 
-  IStorageArea* Configuration::CreateStorageArea()
+  IStorageArea* CreateStorageArea()
   {
     return CreateFilesystemStorage();
   }  
-
-
-  void Configuration::GetConfiguration(Json::Value& result)
-  {
-    boost::recursive_mutex::scoped_lock lock(globalMutex_);
-    result = configuration_;
-  }
-
-
-  void Configuration::FormatConfiguration(std::string& result)
-  {
-    Json::Value config;
-    GetConfiguration(config);
-
-    Json::StyledWriter w;
-    result = w.write(config);
-  }
-
-
-  const FontRegistry& Configuration::GetFontRegistry()
-  {
-    return fontRegistry_;
-  }
-
-
-  void Configuration::SetDefaultEncoding(Encoding encoding)
-  {
-    SetDefaultDicomEncoding(encoding);
-
-    {
-      // Propagate the encoding to the configuration file that is
-      // stored in memory
-      boost::recursive_mutex::scoped_lock lock(globalMutex_);
-      configuration_["DefaultEncoding"] = EnumerationToString(encoding);
-    }
-  }
-
-
-  bool Configuration::HasConfigurationChanged()
-  {
-    Json::Value starting;
-    GetConfiguration(starting);
-
-    Json::Value current;
-    ReadConfiguration(current, configurationFileArg_);
-
-    Json::FastWriter writer;
-    std::string a = writer.write(starting);
-    std::string b = writer.write(current);
-
-    return a != b;
-  }
 }
--- a/OrthancServer/OrthancInitialization.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancInitialization.h	Thu Dec 06 15:58:08 2018 +0100
@@ -33,108 +33,16 @@
 
 #pragma once
 
-#include <string>
-#include <set>
-#include <json/json.h>
-#include <stdint.h>
-
 #include "../Core/FileStorage/IStorageArea.h"
-#include "../Core/HttpServer/MongooseServer.h"
-#include "../Core/Images/FontRegistry.h"
-#include "../Core/WebServiceParameters.h"
-#include "../Core/DicomNetworking/RemoteModalityParameters.h"
-
 #include "IDatabaseWrapper.h"
-#include "ServerEnumerations.h"
-
 
 namespace Orthanc
 {
-  class ServerContext;
-
   void OrthancInitialize(const char* configurationFile = NULL);
 
   void OrthancFinalize();
 
-  class Configuration
-  {
-  private:
-    Configuration();  // Forbidden, this is a static class
-
-  public:
-    static std::string GetGlobalStringParameter(const std::string& parameter,
-                                                const std::string& defaultValue);
-
-    static int GetGlobalIntegerParameter(const std::string& parameter,
-                                         int defaultValue);
-
-    static unsigned int GetGlobalUnsignedIntegerParameter(const std::string& parameter,
-                                                          unsigned int defaultValue);
-
-    static bool GetGlobalBoolParameter(const std::string& parameter,
-                                       bool defaultValue);
-
-    static void GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality,
-                                                  const std::string& name);
-
-    static bool LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality,
-                                                const std::string& aet);
-
-    static bool GetOrthancPeer(WebServiceParameters& peer,
-                               const std::string& name);
-
-    static void GetListOfDicomModalities(std::set<std::string>& target);
-
-    static void GetListOfOrthancPeers(std::set<std::string>& target);
-
-    static void SetupRegisteredUsers(MongooseServer& httpServer);
-
-    static std::string InterpretRelativePath(const std::string& baseDirectory,
-                                             const std::string& relativePath);
-
-    static std::string InterpretStringParameterAsPath(const std::string& parameter);
-
-    static void GetGlobalListOfStringsParameter(std::list<std::string>& target,
-                                                const std::string& key);
+  IDatabaseWrapper* CreateDatabaseWrapper();
 
-    static bool IsKnownAETitle(const std::string& aet,
-                               const std::string& ip);
-
-    static bool IsSameAETitle(const std::string& aet1,
-                              const std::string& aet2);
-
-    static RemoteModalityParameters GetModalityUsingSymbolicName(const std::string& name);
-
-    static RemoteModalityParameters GetModalityUsingAet(const std::string& aet);
-
-    static void UpdateModality(ServerContext& context,
-                               const std::string& symbolicName,
-                               const RemoteModalityParameters& modality);
-
-    static void RemoveModality(ServerContext& context,
-                               const std::string& symbolicName);
-
-    static void UpdatePeer(ServerContext& context,
-                           const std::string& symbolicName,
-                           const WebServiceParameters& peer);
-
-    static void RemovePeer(ServerContext& context,
-                           const std::string& symbolicName);
-
-    static const std::string& GetConfigurationAbsolutePath();
-
-    static IDatabaseWrapper* CreateDatabaseWrapper();
-
-    static IStorageArea* CreateStorageArea();
-
-    static void GetConfiguration(Json::Value& result);
-
-    static void FormatConfiguration(std::string& result);
-
-    static const FontRegistry& GetFontRegistry();
-
-    static void SetDefaultEncoding(Encoding encoding);
-
-    static bool HasConfigurationChanged();
-  };
+  IStorageArea* CreateStorageArea();
 }
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -34,7 +34,7 @@
 #include "PrecompiledHeadersServer.h"
 #include "OrthancMoveRequestHandler.h"
 
-#include "OrthancInitialization.h"
+#include "OrthancConfiguration.h"
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/DicomFormat/DicomArray.h"
 #include "../Core/Logging.h"
@@ -70,7 +70,8 @@
         originatorAet_(originatorAet),
         originatorId_(originatorId)
       {
-        LOG(INFO) << "Sending resource " << publicId << " to modality \"" << targetAet << "\"";
+        LOG(INFO) << "Sending resource " << publicId << " to modality \""
+                  << targetAet << "\" in synchronous mode";
 
         std::list<std::string> tmp;
         context_.GetIndex().GetChildInstances(tmp, publicId);
@@ -81,7 +82,10 @@
           instances_.push_back(*it);
         }
 
-        remote_ = Configuration::GetModalityUsingAet(targetAet);
+        {
+          OrthancConfiguration::ReaderLock lock;
+          remote_ = lock.GetConfiguration().GetModalityUsingAet(targetAet);
+        }
       }
 
       virtual unsigned int GetSubOperationCount() const
@@ -130,12 +134,17 @@
         job_(new DicomModalityStoreJob(context)),
         position_(0)
       {
-        LOG(INFO) << "Sending resource " << publicId << " to modality \"" << targetAet << "\"";
+        LOG(INFO) << "Sending resource " << publicId << " to modality \""
+                  << targetAet << "\" in asynchronous mode";
 
         job_->SetDescription("C-MOVE");
         job_->SetPermissive(true);
         job_->SetLocalAet(context.GetDefaultLocalApplicationEntityTitle());
-        job_->SetRemoteModality(Configuration::GetModalityUsingAet(targetAet));
+
+        {
+          OrthancConfiguration::ReaderLock lock;
+          job_->SetRemoteModality(lock.GetConfiguration().GetModalityUsingAet(targetAet));
+        }
 
         if (originatorId != 0)
         {
@@ -238,7 +247,12 @@
                                               const std::string& originatorAet,
                                               uint16_t originatorId)
   {
-    bool synchronous = Configuration::GetGlobalBoolParameter("SynchronousCMove", false);
+    bool synchronous;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      synchronous = lock.GetConfiguration().GetBooleanParameter("SynchronousCMove", true);
+    }
 
     if (synchronous)
     {
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -208,7 +208,8 @@
 
   static void StoreCreatedInstance(std::string& id /* out */,
                                    RestApiPostCall& call,
-                                   ParsedDicomFile& dicom)
+                                   ParsedDicomFile& dicom,
+                                   bool sendAnswer)
   {
     DicomInstanceToStore toStore;
     toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
@@ -221,6 +222,11 @@
     {
       throw OrthancException(ErrorCode_CannotStoreInstance);
     }
+
+    if (sendAnswer)
+    {
+      OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status);
+    }
   }
 
 
@@ -265,7 +271,7 @@
   {
     if (tags.type() != Json::objectValue)
     {
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest, "Tags field is not an array");
     }
 
     // Inject the user-specified tags
@@ -290,7 +296,7 @@
             tag != DICOM_TAG_STUDY_TIME &&
             dicom.HasTag(tag))
         {
-          throw OrthancException(ErrorCode_CreateDicomOverrideTag);
+          throw OrthancException(ErrorCode_CreateDicomOverrideTag, name);
         }
 
         if (tag == DICOM_TAG_PIXEL_DATA)
@@ -356,7 +362,7 @@
         dicom->ReplacePlainString(DICOM_TAG_INSTANCE_NUMBER, boost::lexical_cast<std::string>(i + 1));
         dicom->ReplacePlainString(DICOM_TAG_IMAGE_INDEX, boost::lexical_cast<std::string>(i + 1));
 
-        StoreCreatedInstance(someInstance, call, *dicom);
+        StoreCreatedInstance(someInstance, call, *dicom, false);
       }
     }
     catch (OrthancException&)
@@ -403,8 +409,8 @@
         const char* tmp = request["Tags"]["SpecificCharacterSet"].asCString();
         if (!GetDicomEncoding(encoding, tmp))
         {
-          LOG(ERROR) << "Unknown specific character set: " << std::string(tmp);
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Unknown specific character set: " + std::string(tmp));
         }
       }
       else
@@ -583,10 +589,7 @@
     }
 
     std::string id;
-    StoreCreatedInstance(id, call, dicom);
-    OrthancRestApi::GetApi(call).AnswerStoredResource(call, id, ResourceType_Instance, StoreStatus_Success);
-
-    return;
+    StoreCreatedInstance(id, call, dicom, true);
   }
 
 
@@ -610,8 +613,7 @@
       CreateDicomV1(dicom, call, request);
 
       std::string id;
-      StoreCreatedInstance(id, call, dicom);
-      OrthancRestApi::GetApi(call).AnswerStoredResource(call, id, ResourceType_Instance, StoreStatus_Success);
+      StoreCreatedInstance(id, call, dicom, true);
     }
   }
 
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -40,20 +40,46 @@
 
 namespace Orthanc
 {
-  void OrthancRestApi::AnswerStoredResource(RestApiPostCall& call,
-                                            const std::string& publicId,
-                                            ResourceType resourceType,
-                                            StoreStatus status) const
+  static void SetupResourceAnswer(Json::Value& result,
+                                  const std::string& publicId,
+                                  ResourceType resourceType,
+                                  StoreStatus status)
   {
-    Json::Value result = Json::objectValue;
+    result = Json::objectValue;
 
     if (status != StoreStatus_Failure)
     {
       result["ID"] = publicId;
       result["Path"] = GetBasePath(resourceType, publicId);
     }
+    
+    result["Status"] = EnumerationToString(status);
+  }
 
-    result["Status"] = EnumerationToString(status);
+
+  void OrthancRestApi::AnswerStoredInstance(RestApiPostCall& call,
+                                            DicomInstanceToStore& instance,
+                                            StoreStatus status) const
+  {
+    Json::Value result;
+    SetupResourceAnswer(result, instance.GetHasher().HashInstance(), 
+                        ResourceType_Instance, status);
+
+    result["ParentPatient"] = instance.GetHasher().HashPatient();
+    result["ParentStudy"] = instance.GetHasher().HashStudy();
+    result["ParentSeries"] = instance.GetHasher().HashSeries();
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  void OrthancRestApi::AnswerStoredResource(RestApiPostCall& call,
+                                            const std::string& publicId,
+                                            ResourceType resourceType,
+                                            StoreStatus status) const
+  {
+    Json::Value result;
+    SetupResourceAnswer(result, publicId, resourceType, status);
     call.GetOutput().AnswerJson(result);
   }
 
@@ -62,14 +88,14 @@
   {
     OrthancRestApi::GetApi(call).leaveBarrier_ = true;
     OrthancRestApi::GetApi(call).resetRequestReceived_ = true;
-    call.GetOutput().AnswerBuffer("{}", "application/json");
+    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
   }
 
 
   void OrthancRestApi::ShutdownOrthanc(RestApiPostCall& call)
   {
     OrthancRestApi::GetApi(call).leaveBarrier_ = true;
-    call.GetOutput().AnswerBuffer("{}", "application/json");
+    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
     LOG(WARNING) << "Shutdown request received";
   }
 
@@ -100,7 +126,7 @@
     std::string publicId;
     StoreStatus status = context.Store(publicId, toStore);
 
-    OrthancRestApi::GetApi(call).AnswerStoredResource(call, publicId, ResourceType_Instance, status);
+    OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status);
   }
 
 
@@ -147,6 +173,105 @@
   static const char* KEY_PRIORITY = "Priority";
   static const char* KEY_SYNCHRONOUS = "Synchronous";
   static const char* KEY_ASYNCHRONOUS = "Asynchronous";
+
+  
+  bool OrthancRestApi::IsSynchronousJobRequest(bool isDefaultSynchronous,
+                                               const Json::Value& body)
+  {
+    if (body.type() != Json::objectValue)
+    {
+      return isDefaultSynchronous;
+    }
+    else if (body.isMember(KEY_SYNCHRONOUS))
+    {
+      return SerializationToolbox::ReadBoolean(body, KEY_SYNCHRONOUS);
+    }
+    else if (body.isMember(KEY_ASYNCHRONOUS))
+    {
+      return !SerializationToolbox::ReadBoolean(body, KEY_ASYNCHRONOUS);
+    }
+    else
+    {
+      return isDefaultSynchronous;
+    }
+  }
+
+  
+  unsigned int OrthancRestApi::GetJobRequestPriority(const Json::Value& body)
+  {
+    if (body.type() != Json::objectValue ||
+        !body.isMember(KEY_PRIORITY))
+    {
+      return 0;   // Default priority
+    }
+    else 
+    {
+      return SerializationToolbox::ReadInteger(body, KEY_PRIORITY);
+    }
+  }
+  
+
+  void OrthancRestApi::SubmitGenericJob(RestApiOutput& output,
+                                        ServerContext& context,
+                                        IJob* job,
+                                        bool synchronous,
+                                        int priority)
+  {
+    std::auto_ptr<IJob> raii(job);
+    
+    if (job == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    if (synchronous)
+    {
+      Json::Value successContent;
+      if (context.GetJobsEngine().GetRegistry().SubmitAndWait
+          (successContent, raii.release(), priority))
+      {
+        // Success in synchronous execution
+        output.AnswerJson(successContent);
+      }
+      else
+      {
+        // Error during synchronous execution
+        output.SignalError(HttpStatus_500_InternalServerError);
+      }
+    }
+    else
+    {
+      // Asynchronous mode: Submit the job, but don't wait for its completion
+      std::string id;
+      context.GetJobsEngine().GetRegistry().Submit
+        (id, raii.release(), priority);
+
+      Json::Value v;
+      v["ID"] = id;
+      v["Path"] = "/jobs/" + id;
+      output.AnswerJson(v);
+    }
+  }
+
+  
+  void OrthancRestApi::SubmitGenericJob(RestApiPostCall& call,
+                                        IJob* job,
+                                        bool isDefaultSynchronous,
+                                        const Json::Value& body) const
+  {
+    std::auto_ptr<IJob> raii(job);
+
+    if (body.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    bool synchronous = IsSynchronousJobRequest(isDefaultSynchronous, body);
+    int priority = GetJobRequestPriority(body);
+
+    SubmitGenericJob(call.GetOutput(), context_, raii.release(), synchronous, priority);
+  }
+
   
   void OrthancRestApi::SubmitCommandsJob(RestApiPostCall& call,
                                          SetOfCommandsJob* job,
@@ -155,11 +280,6 @@
   {
     std::auto_ptr<SetOfCommandsJob> raii(job);
     
-    if (job == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
     if (body.type() != Json::objectValue)
     {
       throw OrthancException(ErrorCode_BadFileFormat);
@@ -176,66 +296,6 @@
       job->SetPermissive(false);
     }
 
-    int priority = 0;
-
-    if (body.isMember(KEY_PRIORITY))
-    {
-      priority = SerializationToolbox::ReadInteger(body, KEY_PRIORITY);
-    }
-
-    bool synchronous = isDefaultSynchronous;
-    
-    if (body.isMember(KEY_SYNCHRONOUS))
-    {
-      synchronous = SerializationToolbox::ReadBoolean(body, KEY_SYNCHRONOUS);
-    }
-    else if (body.isMember(KEY_ASYNCHRONOUS))
-    {
-      synchronous = !SerializationToolbox::ReadBoolean(body, KEY_ASYNCHRONOUS);
-    }
-
-    if (synchronous)
-    {
-      Json::Value successContent;
-      if (context_.GetJobsEngine().GetRegistry().SubmitAndWait
-          (successContent, raii.release(), priority))
-      {
-        // Success in synchronous execution
-        call.GetOutput().AnswerJson(successContent);
-      }
-      else
-      {
-        // Error during synchronous execution
-        call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
-      }
-    }
-    else
-    {
-      // Asynchronous mode: Submit the job, but don't wait for its completion
-      std::string id;
-      context_.GetJobsEngine().GetRegistry().Submit(id, raii.release(), priority);
-
-      Json::Value v;
-      v["ID"] = id;
-      v["Path"] = "/jobs/" + id;
-      call.GetOutput().AnswerJson(v);
-    }
-  }
-  
-
-  void OrthancRestApi::SubmitCommandsJob(RestApiPostCall& call,
-                                         SetOfCommandsJob* job,
-                                         bool isDefaultSynchronous) const
-  {
-    std::auto_ptr<SetOfCommandsJob> raii(job);
-    
-    Json::Value body;
-    
-    if (!call.ParseJsonRequest(body))
-    {
-      body = Json::objectValue;
-    }
-
-    SubmitCommandsJob(call, raii.release(), isDefaultSynchronous, body);
+    SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body);
   }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.h	Thu Dec 06 15:58:08 2018 +0100
@@ -44,6 +44,7 @@
 {
   class ServerContext;
   class ServerIndex;
+  class DicomInstanceToStore;
 
   class OrthancRestApi : public RestApi
   {
@@ -93,18 +94,34 @@
 
     static ServerIndex& GetIndex(RestApiCall& call);
 
+    void AnswerStoredInstance(RestApiPostCall& call,
+                              DicomInstanceToStore& instance,
+                              StoreStatus status) const;
+
     void AnswerStoredResource(RestApiPostCall& call,
                               const std::string& publicId,
                               ResourceType resourceType,
                               StoreStatus status) const;
 
+    static bool IsSynchronousJobRequest(bool isDefaultSynchronous,
+                                        const Json::Value& body);
+    
+    static unsigned int GetJobRequestPriority(const Json::Value& body);
+    
+    static void SubmitGenericJob(RestApiOutput& output,
+                                 ServerContext& context,
+                                 IJob* job,
+                                 bool synchronous,
+                                 int priority);
+    
+    void SubmitGenericJob(RestApiPostCall& call,
+                          IJob* job,
+                          bool isDefaultSynchronous,
+                          const Json::Value& body) const;
+
     void SubmitCommandsJob(RestApiPostCall& call,
                            SetOfCommandsJob* job,
                            bool isDefaultSynchronous,
                            const Json::Value& body) const;
-
-    void SubmitCommandsJob(RestApiPostCall& call,
-                           SetOfCommandsJob* job,
-                           bool isDefaultSynchronous) const;
   };
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -35,40 +35,93 @@
 #include "OrthancRestApi.h"
 
 #include "../../Core/HttpServer/FilesystemHttpSender.h"
+#include "../../Core/SerializationToolbox.h"
 #include "../ServerJobs/ArchiveJob.h"
 
 namespace Orthanc
 {
-  static bool AddResourcesOfInterest(ArchiveJob& job,
-                                     RestApiPostCall& call)
+  static const char* const KEY_RESOURCES = "Resources";
+  static const char* const KEY_EXTENDED = "Extended";
+  
+  static void AddResourcesOfInterestFromArray(ArchiveJob& job,
+                                              const Json::Value& resources)
   {
-    Json::Value resources;
-    if (call.ParseJsonRequest(resources) &&
-        resources.type() == Json::arrayValue)
+    if (resources.type() != Json::arrayValue)
     {
-      for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Expected a list of strings (Orthanc identifiers)");
+    }
+    
+    for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
+    {
+      if (resources[i].type() != Json::stringValue)
       {
-        if (resources[i].type() != Json::stringValue)
-        {
-          return false;   // Bad request
-        }
-
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Expected a list of strings (Orthanc identifiers)");
+      }
+      else
+      {
         job.AddResource(resources[i].asString());
       }
+    }
+  }
 
-      return true;
+  
+  static void AddResourcesOfInterest(ArchiveJob& job         /* inout */,
+                                     const Json::Value& body /* in */)
+  {
+    if (body.type() == Json::arrayValue)
+    {
+      AddResourcesOfInterestFromArray(job, body);
+    }
+    else if (body.type() == Json::objectValue)
+    {
+      if (body.isMember(KEY_RESOURCES))
+      {
+        AddResourcesOfInterestFromArray(job, body[KEY_RESOURCES]);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Missing field " + std::string(KEY_RESOURCES) +
+                               " in the JSON body");
+      }
     }
     else
     {
-      return false;
+      throw OrthancException(ErrorCode_BadFileFormat);
     }
   }
 
 
-  static void SubmitJob(RestApiCall& call,
-                        boost::shared_ptr<TemporaryFile>& tmp,
+  static void GetJobParameters(bool& synchronous,         /* out */
+                               bool& extended,            /* out */
+                               int& priority,             /* out */
+                               const Json::Value& body,   /* in */
+                               const bool defaultExtended /* in */)
+  {
+    synchronous = OrthancRestApi::IsSynchronousJobRequest
+      (true /* synchronous by default */, body);
+
+    priority = OrthancRestApi::GetJobRequestPriority(body);
+
+    if (body.type() == Json::objectValue &&
+        body.isMember(KEY_EXTENDED))
+    {
+      extended = SerializationToolbox::ReadBoolean(body, KEY_EXTENDED);
+    }
+    else
+    {
+      extended = defaultExtended;
+    }
+  }
+
+
+  static void SubmitJob(RestApiOutput& output,
                         ServerContext& context,
                         std::auto_ptr<ArchiveJob>& job,
+                        int priority,
+                        bool synchronous,
                         const std::string& filename)
   {
     if (job.get() == NULL)
@@ -78,94 +131,147 @@
 
     job->SetDescription("REST API");
 
-    Json::Value publicContent;
-    if (context.GetJobsEngine().GetRegistry().SubmitAndWait
-        (publicContent, job.release(), 0 /* TODO priority */))
+    if (synchronous)
     {
-      // The archive is now created: Prepare the sending of the ZIP file
-      FilesystemHttpSender sender(tmp->GetPath());
-      sender.SetContentType("application/zip");
-      sender.SetContentFilename(filename);
+      boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+      job->SetSynchronousTarget(tmp);
+    
+      Json::Value publicContent;
+      if (context.GetJobsEngine().GetRegistry().SubmitAndWait
+          (publicContent, job.release(), priority))
+      {
+        // The archive is now created: Prepare the sending of the ZIP file
+        FilesystemHttpSender sender(tmp->GetPath());
+        sender.SetContentType(MimeType_Gzip);
+        sender.SetContentFilename(filename);
 
-      // Send the ZIP
-      call.GetOutput().AnswerStream(sender);
+        // Send the ZIP
+        output.AnswerStream(sender);
+      }
+      else
+      {
+        output.SignalError(HttpStatus_500_InternalServerError);
+      }
     }
     else
     {
-      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
-    }      
+      OrthancRestApi::SubmitGenericJob(output, context, job.release(), false, priority);
+    }
   }
 
   
-  static void CreateBatchArchive(RestApiPostCall& call)
+  template <bool IS_MEDIA,
+            bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
+  static void CreateBatch(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
-    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, false, false));
-
-    if (AddResourcesOfInterest(*job, call))
+    Json::Value body;
+    if (call.ParseJsonRequest(body))
     {
-      SubmitJob(call, tmp, context, job, "Archive.zip");
+      bool synchronous, extended;
+      int priority;
+      GetJobParameters(synchronous, extended, priority, body, DEFAULT_IS_EXTENDED);
+      
+      std::auto_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
+      AddResourcesOfInterest(*job, body);
+      SubmitJob(call.GetOutput(), context, job, priority, synchronous, "Archive.zip");
     }
-  }  
-
-  
-  template <bool Extended>
-  static void CreateBatchMedia(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
-    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, true, Extended));
-
-    if (AddResourcesOfInterest(*job, call))
+    else
     {
-      SubmitJob(call, tmp, context, job, "Archive.zip");
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Expected a list of resources to archive in the body");
     }
   }
   
 
-  static void CreateArchive(RestApiGetCall& call)
+  template <bool IS_MEDIA,
+            bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
+  static void CreateSingleGet(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string id = call.GetUriComponent("id", "");
 
-    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
-    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, false, false));
+    bool extended;
+    if (IS_MEDIA)
+    {
+      extended = call.HasArgument("extended");
+    }
+    else
+    {
+      extended = false;
+    }
+    
+    std::auto_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
     job->AddResource(id);
 
-    SubmitJob(call, tmp, context, job, id + ".zip");
+    SubmitJob(call.GetOutput(), context, job, 0 /* priority */,
+              true /* synchronous */, id + ".zip");
   }
 
 
-  static void CreateMedia(RestApiGetCall& call)
+  template <bool IS_MEDIA,
+            bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
+  static void CreateSinglePost(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string id = call.GetUriComponent("id", "");
 
-    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
-    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, true, call.HasArgument("extended")));
-    job->AddResource(id);
-
-    SubmitJob(call, tmp, context, job, id + ".zip");
+    Json::Value body;
+    if (call.ParseJsonRequest(body))
+    {
+      bool synchronous, extended;
+      int priority;
+      GetJobParameters(synchronous, extended, priority, body, DEFAULT_IS_EXTENDED);
+      
+      std::auto_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
+      job->AddResource(id);
+      SubmitJob(call.GetOutput(), context, job, priority, synchronous, id + ".zip");
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
   }
 
-
+    
   void OrthancRestApi::RegisterArchive()
   {
-    Register("/patients/{id}/archive", CreateArchive);
-    Register("/studies/{id}/archive", CreateArchive);
-    Register("/series/{id}/archive", CreateArchive);
+    Register("/patients/{id}/archive",
+             CreateSingleGet<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+    Register("/studies/{id}/archive",
+             CreateSingleGet<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+    Register("/series/{id}/archive",
+             CreateSingleGet<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+
+    Register("/patients/{id}/archive",
+             CreateSinglePost<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+    Register("/studies/{id}/archive",
+             CreateSinglePost<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+    Register("/series/{id}/archive",
+             CreateSinglePost<false /* ZIP */, false /* extended makes no sense in ZIP */>);
 
-    Register("/patients/{id}/media", CreateMedia);
-    Register("/studies/{id}/media", CreateMedia);
-    Register("/series/{id}/media", CreateMedia);
+    Register("/patients/{id}/media",
+             CreateSingleGet<true /* media */, false /* not extended by default */>);
+    Register("/studies/{id}/media",
+             CreateSingleGet<true /* media */, false /* not extended by default */>);
+    Register("/series/{id}/media",
+             CreateSingleGet<true /* media */, false /* not extended by default */>);
 
-    Register("/tools/create-archive", CreateBatchArchive);
-    Register("/tools/create-media", CreateBatchMedia<false>);
-    Register("/tools/create-media-extended", CreateBatchMedia<true>);
+    Register("/patients/{id}/media",
+             CreateSinglePost<true /* media */, false /* not extended by default */>);
+    Register("/studies/{id}/media",
+             CreateSinglePost<true /* media */, false /* not extended by default */>);
+    Register("/series/{id}/media",
+             CreateSinglePost<true /* media */, false /* not extended by default */>);
+
+    Register("/tools/create-archive",
+             CreateBatch<false /* ZIP */,  false /* extended makes no sense in ZIP */>);
+    Register("/tools/create-media",
+             CreateBatch<true /* media */, false /* not extended by default */>);
+    Register("/tools/create-media-extended",
+             CreateBatch<true /* media */, true /* extended by default */>);
   }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -98,7 +98,7 @@
   static void DeleteChanges(RestApiDeleteCall& call)
   {
     OrthancRestApi::GetIndex(call).DeleteChanges();
-    call.GetOutput().AnswerBuffer("", "text/plain");
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
 
@@ -130,7 +130,7 @@
   static void DeleteExports(RestApiDeleteCall& call)
   {
     OrthancRestApi::GetIndex(call).DeleteExportedResources();
-    call.GetOutput().AnswerBuffer("", "text/plain");
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
   
 
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -37,7 +37,7 @@
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/Logging.h"
 #include "../../Core/SerializationToolbox.h"
-#include "../OrthancInitialization.h"
+#include "../OrthancConfiguration.h"
 #include "../QueryRetrieveHandler.h"
 #include "../ServerJobs/DicomModalityStoreJob.h"
 #include "../ServerJobs/DicomMoveScuJob.h"
@@ -47,6 +47,13 @@
 
 namespace Orthanc
 {
+  static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name)
+  {
+    OrthancConfiguration::ReaderLock lock;
+    return lock.GetConfiguration().GetModalityUsingSymbolicName(name);
+  }
+
+
   /***************************************************************************
    * DICOM C-Echo SCU
    ***************************************************************************/
@@ -57,7 +64,7 @@
 
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
     RemoteModalityParameters remote =
-      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     try
     {
@@ -67,7 +74,7 @@
       if (connection.Echo())
       {
         // Echo has succeeded
-        call.GetOutput().AnswerBuffer("{}", "application/json");
+        call.GetOutput().AnswerBuffer("{}", MimeType_Json);
         return;
       }
     }
@@ -181,7 +188,7 @@
 
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
     RemoteModalityParameters remote =
-      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
     
     DicomFindAnswers answers(false);
 
@@ -216,7 +223,7 @@
       
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
     RemoteModalityParameters remote =
-      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     DicomFindAnswers answers(false);
 
@@ -252,7 +259,7 @@
          
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
     RemoteModalityParameters remote =
-      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     DicomFindAnswers answers(false);
 
@@ -289,7 +296,7 @@
          
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
     RemoteModalityParameters remote =
-      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     DicomFindAnswers answers(false);
 
@@ -331,7 +338,7 @@
  
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
     RemoteModalityParameters remote =
-      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     DicomUserConnection connection(localAet, remote);
     connection.Open();
@@ -491,13 +498,30 @@
 
   static void ListQueryAnswers(RestApiGetCall& call)
   {
+    const bool expand = call.HasArgument("expand");
+    const bool simplify = call.HasArgument("simplify");
+    
     QueryAccessor query(call);
     size_t count = query.GetHandler().GetAnswersCount();
 
     Json::Value result = Json::arrayValue;
     for (size_t i = 0; i < count; i++)
     {
-      result.append(boost::lexical_cast<std::string>(i));
+      if (expand)
+      {
+        // New in Orthanc 1.4.3
+        DicomMap value;
+        query.GetHandler().GetAnswer(value, i);
+        
+        Json::Value json = Json::objectValue;
+        FromDcmtkBridge::ToJson(json, value, simplify);
+
+        result.append(json);
+      }
+      else
+      {
+        result.append(boost::lexical_cast<std::string>(i));
+      }
     }
 
     call.GetOutput().AnswerJson(result);
@@ -589,14 +613,14 @@
   static void GetQueryLevel(RestApiGetCall& call)
   {
     QueryAccessor query(call);
-    call.GetOutput().AnswerBuffer(EnumerationToString(query.GetHandler().GetLevel()), "text/plain");
+    call.GetOutput().AnswerBuffer(EnumerationToString(query.GetHandler().GetLevel()), MimeType_PlainText);
   }
 
 
   static void GetQueryModality(RestApiGetCall& call)
   {
     QueryAccessor query(call);
-    call.GetOutput().AnswerBuffer(query.GetHandler().GetModalitySymbolicName(), "text/plain");
+    call.GetOutput().AnswerBuffer(query.GetHandler().GetModalitySymbolicName(), MimeType_PlainText);
   }
 
 
@@ -604,7 +628,7 @@
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
     context.GetQueryRetrieveArchive().Remove(call.GetUriComponent("id", ""));
-    call.GetOutput().AnswerBuffer("", "text/plain");
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
 
@@ -696,6 +720,13 @@
       }
     }
 
+    bool logExportedResources;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      logExportedResources = lock.GetConfiguration().GetBooleanParameter("LogExportedResources", false);
+    }
+
     for (Json::Value::ArrayIndex i = 0; i < resources->size(); i++)
     {
       if (!(*resources) [i].isString())
@@ -709,7 +740,7 @@
         return false;
       }
 
-      if (Configuration::GetGlobalBoolParameter("LogExportedResources", false))
+      if (logExportedResources)
       {
         context.GetIndex().LogExportedResource(stripped, remote);
       }
@@ -740,7 +771,7 @@
         (request, "MoveOriginatorID", 0 /* By default, not a C-MOVE */);
 
       job->SetLocalAet(localAet);
-      job->SetRemoteModality(Configuration::GetModalityUsingSymbolicName(remote));
+      job->SetRemoteModality(MyGetModalityUsingSymbolicName(remote));
 
       if (moveOriginatorID != 0)
       {
@@ -784,7 +815,7 @@
       (request, "TargetAet", context.GetDefaultLocalApplicationEntityTitle());
 
     const RemoteModalityParameters source =
-      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     DicomUserConnection connection(localAet, source);
     connection.Open();
@@ -798,7 +829,7 @@
     }
 
     // Move has succeeded
-    call.GetOutput().AnswerBuffer("{}", "application/json");
+    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
   }
 
 
@@ -815,8 +846,10 @@
 
   static void ListPeers(RestApiGetCall& call)
   {
+    OrthancConfiguration::ReaderLock lock;
+
     OrthancRestApi::SetOfStrings peers;
-    Configuration::GetListOfOrthancPeers(peers);
+    lock.GetConfiguration().GetListOfOrthancPeers(peers);
 
     if (call.HasArgument("expand"))
     {
@@ -826,7 +859,7 @@
       {
         WebServiceParameters peer;
         
-        if (Configuration::GetOrthancPeer(peer, *it))
+        if (lock.GetConfiguration().LookupOrthancPeer(peer, *it))
         {
           Json::Value jsonPeer = Json::objectValue;
           // only return the minimum information to identify the
@@ -857,8 +890,10 @@
 
   static void ListPeerOperations(RestApiGetCall& call)
   {
+    OrthancConfiguration::ReaderLock lock;
+
     OrthancRestApi::SetOfStrings peers;
-    Configuration::GetListOfOrthancPeers(peers);
+    lock.GetConfiguration().GetListOfOrthancPeers(peers);
 
     std::string id = call.GetUriComponent("id", "");
     if (IsExistingPeer(peers, id))
@@ -878,8 +913,10 @@
 
     if (GetInstancesToExport(request, *job, remote, call))
     {
+      OrthancConfiguration::ReaderLock lock;
+
       WebServiceParameters peer;
-      if (Configuration::GetOrthancPeer(peer, remote))
+      if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
       {
         job->SetPeer(peer);    
         OrthancRestApi::GetApi(call).SubmitCommandsJob
@@ -887,8 +924,8 @@
       }
       else
       {
-        LOG(ERROR) << "No peer with symbolic name: " << remote;
-        throw OrthancException(ErrorCode_UnknownResource);
+        throw OrthancException(ErrorCode_UnknownResource,
+                               "No peer with symbolic name: " + remote);
       }
     }
   }
@@ -904,8 +941,10 @@
 
   static void ListModalities(RestApiGetCall& call)
   {
+    OrthancConfiguration::ReaderLock lock;
+
     OrthancRestApi::SetOfStrings modalities;
-    Configuration::GetListOfDicomModalities(modalities);
+    lock.GetConfiguration().GetListOfDicomModalities(modalities);
 
     if (call.HasArgument("expand"))
     {
@@ -913,7 +952,7 @@
       for (OrthancRestApi::SetOfStrings::const_iterator
              it = modalities.begin(); it != modalities.end(); ++it)
       {
-        const RemoteModalityParameters& remote = Configuration::GetModalityUsingSymbolicName(*it);
+        const RemoteModalityParameters& remote = lock.GetConfiguration().GetModalityUsingSymbolicName(*it);
         
         Json::Value info;
         remote.Serialize(info, true /* force advanced format */);
@@ -936,8 +975,10 @@
 
   static void ListModalityOperations(RestApiGetCall& call)
   {
+    OrthancConfiguration::ReaderLock lock;
+
     OrthancRestApi::SetOfStrings modalities;
-    Configuration::GetListOfDicomModalities(modalities);
+    lock.GetConfiguration().GetListOfDicomModalities(modalities);
 
     std::string id = call.GetUriComponent("id", "");
     if (IsExistingModality(modalities, id))
@@ -957,8 +998,15 @@
     {
       RemoteModalityParameters modality;
       modality.Unserialize(json);
-      Configuration::UpdateModality(context, call.GetUriComponent("id", ""), modality);
-      call.GetOutput().AnswerBuffer("", "text/plain");
+
+      {
+        OrthancConfiguration::WriterLock lock;
+        lock.GetConfiguration().UpdateModality(call.GetUriComponent("id", ""), modality);
+      }
+
+      context.SignalUpdatedModalities();
+
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
     }
   }
 
@@ -967,8 +1015,14 @@
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    Configuration::RemoveModality(context, call.GetUriComponent("id", ""));
-    call.GetOutput().AnswerBuffer("", "text/plain");
+    {
+      OrthancConfiguration::WriterLock lock;
+      lock.GetConfiguration().RemoveModality(call.GetUriComponent("id", ""));
+    }
+
+    context.SignalUpdatedModalities();
+
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
 
@@ -982,8 +1036,15 @@
     {
       WebServiceParameters peer;
       peer.Unserialize(json);
-      Configuration::UpdatePeer(context, call.GetUriComponent("id", ""), peer);
-      call.GetOutput().AnswerBuffer("", "text/plain");
+
+      {
+        OrthancConfiguration::WriterLock lock;
+        lock.GetConfiguration().UpdatePeer(call.GetUriComponent("id", ""), peer);
+      }
+
+      context.SignalUpdatedPeers();
+
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
     }
   }
 
@@ -992,8 +1053,14 @@
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    Configuration::RemovePeer(context, call.GetUriComponent("id", ""));
-    call.GetOutput().AnswerBuffer("", "text/plain");
+    {
+      OrthancConfiguration::WriterLock lock;
+      lock.GetConfiguration().RemovePeer(call.GetUriComponent("id", ""));
+    }
+
+    context.SignalUpdatedPeers();
+
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
 
@@ -1006,7 +1073,7 @@
     {
       const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
       RemoteModalityParameters remote =
-        Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+        MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
       std::auto_ptr<ParsedDicomFile> query(ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0)));
 
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -39,7 +39,7 @@
 #include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
 #include "../../Core/HttpServer/HttpContentNegociation.h"
 #include "../../Core/Logging.h"
-#include "../OrthancInitialization.h"
+#include "../OrthancConfiguration.h"
 #include "../Search/LookupResource.h"
 #include "../ServerContext.h"
 #include "../ServerToolbox.h"
@@ -151,14 +151,16 @@
     {
       if (!call.HasArgument("limit"))
       {
-        LOG(ERROR) << "Missing \"limit\" argument for GET request against: " << call.FlattenUri();
-        throw OrthancException(ErrorCode_BadRequest);
+        throw OrthancException(ErrorCode_BadRequest,
+                               "Missing \"limit\" argument for GET request against: " +
+                               call.FlattenUri());
       }
 
       if (!call.HasArgument("since"))
       {
-        LOG(ERROR) << "Missing \"since\" argument for GET request against: " << call.FlattenUri();
-        throw OrthancException(ErrorCode_BadRequest);
+        throw OrthancException(ErrorCode_BadRequest,
+                               "Missing \"since\" argument for GET request against: " +
+                               call.FlattenUri());
       }
 
       size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", ""));
@@ -201,7 +203,7 @@
   {
     std::string publicId = call.GetUriComponent("id", "");
     bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId);
-    call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", "text/plain");
+    call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", MimeType_PlainText);
   }
 
 
@@ -218,12 +220,12 @@
     if (body == "0")
     {
       context.GetIndex().SetProtectedPatient(publicId, false);
-      call.GetOutput().AnswerBuffer("", "text/plain");
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
     }
     else if (body == "1")
     {
       context.GetIndex().SetProtectedPatient(publicId, true);
-      call.GetOutput().AnswerBuffer("", "text/plain");
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
     }
     else
     {
@@ -256,7 +258,7 @@
     call.BodyToString(target);
     SystemToolbox::WriteFile(dicom, target);
 
-    call.GetOutput().AnswerBuffer("{}", "application/json");
+    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
   }
 
 
@@ -284,7 +286,7 @@
       // is present
       std::string full;
       context.ReadDicomAsJson(full, publicId);
-      call.GetOutput().AnswerBuffer(full, "application/json");
+      call.GetOutput().AnswerBuffer(full, MimeType_Json);
     }
   }
 
@@ -340,7 +342,7 @@
       std::auto_ptr<ImageAccessor>&  image_;
       ImageExtractionMode            mode_;
       bool                           invert_;
-      std::string                    format_;
+      MimeType                       format_;
       std::string                    answer_;
 
     public:
@@ -360,27 +362,19 @@
 
       void EncodeUsingPng()
       {
-        format_ = "image/png";
+        format_ = MimeType_Png;
         DicomImageDecoder::ExtractPngImage(answer_, image_, mode_, invert_);
       }
 
       void EncodeUsingPam()
       {
-        /**
-         * "No Internet Media Type (aka MIME type, content type) for
-         * PBM has been registered with IANA, but the unofficial value
-         * image/x-portable-arbitrarymap is assigned by this
-         * specification, to be consistent with conventional values
-         * for the older Netpbm formats."
-         * http://netpbm.sourceforge.net/doc/pam.html
-         **/
-        format_ = "image/x-portable-arbitrarymap";
+        format_ = MimeType_Pam;
         DicomImageDecoder::ExtractPamImage(answer_, image_, mode_, invert_);
       }
 
       void EncodeUsingJpeg(uint8_t quality)
       {
-        format_ = "image/jpeg";
+        format_ = MimeType_Jpeg;
         DicomImageDecoder::ExtractJpegImage(answer_, image_, mode_, invert_, quality);
       }
     };
@@ -448,8 +442,9 @@
 
         if (!ok)
         {
-          LOG(ERROR) << "Bad quality for a JPEG encoding (must be a number between 0 and 100): " << v;
-          throw OrthancException(ErrorCode_BadRequest);
+          throw OrthancException(
+            ErrorCode_BadRequest,
+            "Bad quality for a JPEG encoding (must be a number between 0 and 100): " + v);
         }
       }
 
@@ -537,7 +532,7 @@
     }
     catch (OrthancException& e)
     {
-      if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange)
+      if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange || e.GetErrorCode() == ErrorCode_UnknownResource)
       {
         // The frame number is out of the range for this DICOM
         // instance, the resource is not existent
@@ -552,19 +547,20 @@
 
         call.GetOutput().Redirect(root + "app/images/unsupported.png");
       }
+      return;
     }
 
     ImageToEncode image(decoded, mode, invert);
 
     HttpContentNegociation negociation;
     EncodePng png(image);
-    negociation.Register("image/png", png);
+    negociation.Register(MIME_PNG, png);
 
     EncodeJpeg jpeg(image, call);
-    negociation.Register("image/jpeg", jpeg);
+    negociation.Register(MIME_JPEG, jpeg);
 
     EncodePam pam(image);
-    negociation.Register("image/x-portable-arbitrarymap", pam);
+    negociation.Register(MIME_PAM, pam);
 
     if (negociation.Apply(call.GetHttpHeaders()))
     {
@@ -604,7 +600,7 @@
     std::string result;
     decoded->ToMatlabString(result);
 
-    call.GetOutput().AnswerBuffer(result, "text/plain");
+    call.GetOutput().AnswerBuffer(result, MimeType_PlainText);
   }
 
 
@@ -624,7 +620,8 @@
     }
 
     std::string publicId = call.GetUriComponent("id", "");
-    std::string raw, mime;
+    std::string raw;
+    MimeType mime;
 
     {
       ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
@@ -636,7 +633,7 @@
       GzipCompressor gzip;
       std::string compressed;
       gzip.Compress(compressed, raw.empty() ? NULL : raw.c_str(), raw.size());
-      call.GetOutput().AnswerBuffer(compressed, "application/gzip");
+      call.GetOutput().AnswerBuffer(compressed, MimeType_Gzip);
     }
     else
     {
@@ -717,7 +714,7 @@
     std::string value;
     if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, metadata))
     {
-      call.GetOutput().AnswerBuffer(value, "text/plain");
+      call.GetOutput().AnswerBuffer(value, MimeType_PlainText);
     }
   }
 
@@ -733,7 +730,7 @@
     if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
     {      
       OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata);
-      call.GetOutput().AnswerBuffer("", "text/plain");
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
     }
     else
     {
@@ -757,7 +754,7 @@
     {
       // It is forbidden to modify internal metadata
       OrthancRestApi::GetIndex(call).SetMetadata(publicId, metadata, value);
-      call.GetOutput().AnswerBuffer("", "text/plain");
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
     }
     else
     {
@@ -858,7 +855,7 @@
       // Return the raw data (possibly compressed), as stored on the filesystem
       std::string content;
       context.ReadAttachment(content, publicId, type, false);
-      call.GetOutput().AnswerBuffer(content, "application/octet-stream");
+      call.GetOutput().AnswerBuffer(content, MimeType_Binary);
     }
   }
 
@@ -868,7 +865,7 @@
     FileInfo info;
     if (GetAttachmentInfo(info, call))
     {
-      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), "text/plain");
+      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), MimeType_PlainText);
     }
   }
 
@@ -878,7 +875,7 @@
     FileInfo info;
     if (GetAttachmentInfo(info, call))
     {
-      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), "text/plain");
+      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), MimeType_PlainText);
     }
   }
 
@@ -889,7 +886,7 @@
     if (GetAttachmentInfo(info, call) &&
         info.GetUncompressedMD5() != "")
     {
-      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), "text/plain");
+      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), MimeType_PlainText);
     }
   }
 
@@ -900,7 +897,7 @@
     if (GetAttachmentInfo(info, call) &&
         info.GetCompressedMD5() != "")
     {
-      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), "text/plain");
+      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), MimeType_PlainText);
     }
   }
 
@@ -950,7 +947,7 @@
     if (ok)
     {
       LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has the right MD5";
-      call.GetOutput().AnswerBuffer("{}", "application/json");
+      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
     }
     else
     {
@@ -971,7 +968,7 @@
     if (IsUserContentType(contentType) &&  // It is forbidden to modify internal attachments
         context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize()))
     {
-      call.GetOutput().AnswerBuffer("{}", "application/json");
+      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
     }
     else
     {
@@ -993,23 +990,28 @@
     {
       allowed = true;
     }
-    else if (Configuration::GetGlobalBoolParameter("StoreDicom", true) &&
-             contentType == FileContentType_DicomAsJson)
-    {
-      allowed = true;
-    }
     else
     {
-      // It is forbidden to delete internal attachments, except for
-      // the "DICOM as JSON" summary as of Orthanc 1.2.0 (this summary
-      // would be automatically reconstructed on the next GET call)
-      allowed = false;
+      OrthancConfiguration::ReaderLock lock;
+
+      if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true) &&
+          contentType == FileContentType_DicomAsJson)
+      {
+        allowed = true;
+      }
+      else
+      {
+        // It is forbidden to delete internal attachments, except for
+        // the "DICOM as JSON" summary as of Orthanc 1.2.0 (this summary
+        // would be automatically reconstructed on the next GET call)
+        allowed = false;
+      }
     }
 
     if (allowed) 
     {
       OrthancRestApi::GetIndex(call).DeleteAttachment(publicId, contentType);
-      call.GetOutput().AnswerBuffer("{}", "application/json");
+      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
     }
     else
     {
@@ -1028,7 +1030,7 @@
     FileContentType contentType = StringToContentType(name);
 
     OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression);
-    call.GetOutput().AnswerBuffer("{}", "application/json");
+    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
   }
 
 
@@ -1038,7 +1040,7 @@
     if (GetAttachmentInfo(info, call))
     {
       std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1";
-      call.GetOutput().AnswerBuffer(answer, "text/plain");
+      call.GetOutput().AnswerBuffer(answer, MimeType_PlainText);
     }
   }
 
@@ -1475,7 +1477,7 @@
 
     if (locker.GetDicom().ExtractPdf(pdf))
     {
-      call.GetOutput().AnswerBuffer(pdf, "application/pdf");
+      call.GetOutput().AnswerBuffer(pdf, MimeType_Pdf);
       return;
     }
   }
@@ -1537,7 +1539,7 @@
       }
     }
 
-    call.GetOutput().AnswerBuffer("", "text/plain");
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
 
@@ -1546,7 +1548,7 @@
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
     ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""));
-    call.GetOutput().AnswerBuffer("", "text/plain");
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
 
@@ -1563,7 +1565,7 @@
       ServerToolbox::ReconstructResource(context, *study);
     }
     
-    call.GetOutput().AnswerBuffer("", "text/plain");
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
 
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -34,7 +34,7 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
-#include "../OrthancInitialization.h"
+#include "../OrthancConfiguration.h"
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Plugins/Engine/PluginsManager.h"
 #include "../../Plugins/Engine/OrthancPlugins.h"
@@ -55,12 +55,16 @@
     Json::Value result = Json::objectValue;
 
     result["ApiVersion"] = ORTHANC_API_VERSION;
+    result["Version"] = ORTHANC_VERSION;
     result["DatabaseVersion"] = OrthancRestApi::GetIndex(call).GetDatabaseVersion();
-    result["DicomAet"] = Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC");
-    result["DicomPort"] = Configuration::GetGlobalUnsignedIntegerParameter("DicomPort", 4242);
-    result["HttpPort"] = Configuration::GetGlobalUnsignedIntegerParameter("HttpPort", 8042);
-    result["Name"] = Configuration::GetGlobalStringParameter("Name", "");
-    result["Version"] = ORTHANC_VERSION;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      result["DicomAet"] = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC");
+      result["DicomPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242);
+      result["HttpPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042);
+      result["Name"] = lock.GetConfiguration().GetStringParameter("Name", "");
+    }
 
     result["StorageAreaPlugin"] = Json::nullValue;
     result["DatabaseBackendPlugin"] = Json::nullValue;
@@ -99,19 +103,19 @@
     std::string level = call.GetArgument("level", "");
     if (level == "patient")
     {
-      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient), "text/plain");
+      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient), MimeType_PlainText);
     }
     else if (level == "study")
     {
-      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study), "text/plain");
+      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study), MimeType_PlainText);
     }
     else if (level == "series")
     {
-      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series), "text/plain");
+      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series), MimeType_PlainText);
     }
     else if (level == "instance")
     {
-      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance), "text/plain");
+      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance), MimeType_PlainText);
     }
   }
 
@@ -128,13 +132,13 @@
       lock.GetLua().Execute(result, command);
     }
 
-    call.GetOutput().AnswerBuffer(result, "text/plain");
+    call.GetOutput().AnswerBuffer(result, MimeType_PlainText);
   }
 
   template <bool UTC>
   static void GetNowIsoString(RestApiGetCall& call)
   {
-    call.GetOutput().AnswerBuffer(SystemToolbox::GetNowIsoString(UTC), "text/plain");
+    call.GetOutput().AnswerBuffer(SystemToolbox::GetNowIsoString(UTC), MimeType_PlainText);
   }
 
 
@@ -142,14 +146,14 @@
   {
     std::string statement;
     GetFileResource(statement, EmbeddedResources::DICOM_CONFORMANCE_STATEMENT);
-    call.GetOutput().AnswerBuffer(statement, "text/plain");
+    call.GetOutput().AnswerBuffer(statement, MimeType_PlainText);
   }
 
 
   static void GetDefaultEncoding(RestApiGetCall& call)
   {
     Encoding encoding = GetDefaultDicomEncoding();
-    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), MimeType_PlainText);
   }
 
 
@@ -157,9 +161,12 @@
   {
     Encoding encoding = StringToEncoding(call.GetBodyData());
 
-    Configuration::SetDefaultEncoding(encoding);
+    {
+      OrthancConfiguration::WriterLock lock;
+      lock.GetConfiguration().SetDefaultEncoding(encoding);
+    }
 
-    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), MimeType_PlainText);
   }
 
 
@@ -265,7 +272,7 @@
 #endif
     }
 
-    call.GetOutput().AnswerBuffer(s, "application/javascript");
+    call.GetOutput().AnswerBuffer(s, MimeType_JavaScript);
   }
 
 
@@ -318,6 +325,27 @@
   }
 
 
+  static void GetJobOutput(RestApiGetCall& call)
+  {
+    std::string job = call.GetUriComponent("id", "");
+    std::string key = call.GetUriComponent("key", "");
+
+    std::string value;
+    MimeType mime;
+    
+    if (OrthancRestApi::GetContext(call).GetJobsEngine().
+        GetRegistry().GetJobOutput(value, mime, job, key))
+    {
+      call.GetOutput().AnswerBuffer(value, mime);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentItem,
+                             "Job has no such output: " + key);
+    }
+  }
+
+
   enum JobAction
   {
     JobAction_Cancel,
@@ -357,7 +385,7 @@
     
     if (ok)
     {
-      call.GetOutput().AnswerBuffer("{}", "application/json");
+      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
     }
   }
 
@@ -385,5 +413,6 @@
     Register("/jobs/{id}/pause", ApplyJobAction<JobAction_Pause>);
     Register("/jobs/{id}/resubmit", ApplyJobAction<JobAction_Resubmit>);
     Register("/jobs/{id}/resume", ApplyJobAction<JobAction_Resume>);
+    Register("/jobs/{id}/{key}", GetJobOutput);
   }
 }
--- a/OrthancServer/QueryRetrieveHandler.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/QueryRetrieveHandler.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -34,7 +34,7 @@
 #include "PrecompiledHeadersServer.h"
 #include "QueryRetrieveHandler.h"
 
-#include "OrthancInitialization.h"
+#include "OrthancConfiguration.h"
 
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/Logging.h"
@@ -119,7 +119,11 @@
   {
     Invalidate();
     modalityName_ = symbolicName;
-    Configuration::GetDicomModalityUsingSymbolicName(modality_, symbolicName);
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      lock.GetConfiguration().GetDicomModalityUsingSymbolicName(modality_, symbolicName);
+    }
   }
 
 
--- a/OrthancServer/Search/HierarchicalMatcher.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/Search/HierarchicalMatcher.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -38,7 +38,7 @@
 #include "../../Core/OrthancException.h"
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/DicomParsing/ToDcmtkBridge.h"
-#include "../OrthancInitialization.h"
+#include "../OrthancConfiguration.h"
 
 #include <dcmtk/dcmdata/dcfilefo.h>
 
@@ -46,9 +46,14 @@
 {
   HierarchicalMatcher::HierarchicalMatcher(ParsedDicomFile& query)
   {
-    Setup(*query.GetDcmtkObject().getDataset(), 
-          Configuration::GetGlobalBoolParameter("CaseSensitivePN", false),
-          query.GetEncoding());
+    bool caseSensitivePN;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false);
+    }
+
+    Setup(*query.GetDcmtkObject().getDataset(), caseSensitivePN, query.GetEncoding());
   }
 
 
--- a/OrthancServer/ServerContext.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerContext.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -40,7 +40,7 @@
 #include "../Core/HttpServer/HttpStreamTranscoder.h"
 #include "../Core/Logging.h"
 #include "../Plugins/Engine/OrthancPlugins.h"
-#include "OrthancInitialization.h"
+#include "OrthancConfiguration.h"
 #include "OrthancRestApi/OrthancRestApi.h"
 #include "Search/LookupResource.h"
 #include "ServerJobs/OrthancJobUnserializer.h"
@@ -170,8 +170,7 @@
         }
         catch (OrthancException& e)
         {
-          LOG(ERROR) << "Cannot unserialize the jobs engine: " << e.What();
-          throw;
+          LOG(WARNING) << "Cannot unserialize the jobs engine, starting anyway: " << e.What();
         }
       }
       else
@@ -181,11 +180,9 @@
     }
     else
     {
-      LOG(WARNING) << "Not reloading the jobs from the last execution of Orthanc";
+      LOG(INFO) << "Not reloading the jobs from the last execution of Orthanc";
     }
 
-    //jobsEngine_.GetRegistry().SetMaxCompleted   // TODO
-
     jobsEngine_.GetRegistry().SetObserver(*this);
     jobsEngine_.Start();
     isJobsEngineUnserialized_ = true;
@@ -217,7 +214,8 @@
 
   ServerContext::ServerContext(IDatabaseWrapper& database,
                                IStorageArea& area,
-                               bool unitTesting) :
+                               bool unitTesting,
+                               size_t maxCompletedJobs) :
     index_(*this, database, (unitTesting ? 20 : 500)),
     area_(area),
     compressionEnabled_(false),
@@ -227,16 +225,25 @@
     mainLua_(*this),
     filterLua_(*this),
     luaListener_(*this),
+    jobsEngine_(maxCompletedJobs),
 #if ORTHANC_ENABLE_PLUGINS == 1
     plugins_(NULL),
 #endif
     done_(false),
     haveJobsChanged_(false),
-    isJobsEngineUnserialized_(false),
-    queryRetrieveArchive_(Configuration::GetGlobalUnsignedIntegerParameter("QueryRetrieveSize", 10)),
-    defaultLocalAet_(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"))
+    isJobsEngineUnserialized_(false)
   {
-    jobsEngine_.SetWorkersCount(Configuration::GetGlobalUnsignedIntegerParameter("ConcurrentJobs", 2));
+    {
+      OrthancConfiguration::ReaderLock lock;
+
+      queryRetrieveArchive_.reset(
+        new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 10)));
+      mediaArchive_.reset(
+        new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1)));
+      defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC");
+      jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2));
+    }
+
     jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
 
     listeners_.push_back(ServerListener(luaListener_, "Lua"));
@@ -316,10 +323,7 @@
     {
       StorageAccessor accessor(area_);
 
-      {
-        DicomInstanceHasher hasher(dicom.GetSummary());
-        resultPublicId = hasher.HashInstance();
-      }
+      resultPublicId = dicom.GetHasher().HashInstance();
 
       Json::Value simplifiedTags;
       ServerToolbox::SimplifyTags(simplifiedTags, dicom.GetJson(), DicomToJsonFormat_Human);
@@ -537,8 +541,8 @@
       if (!AddAttachment(instancePublicId, FileContentType_DicomAsJson,
                          result.c_str(), result.size()))
       {
-        LOG(WARNING) << "Cannot associate the DICOM-as-JSON summary to instance: " << instancePublicId;
-        throw OrthancException(ErrorCode_InternalError);
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot associate the DICOM-as-JSON summary to instance: " + instancePublicId);
       }
     }
   }
@@ -597,8 +601,9 @@
     FileInfo attachment;
     if (!index_.LookupAttachment(attachment, instancePublicId, content))
     {
-      LOG(WARNING) << "Unable to read attachment " << EnumerationToString(content) << " of instance " << instancePublicId;
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Unable to read attachment " + EnumerationToString(content) +
+                             " of instance " + instancePublicId);
     }
 
     if (uncompressIfNeeded)
@@ -825,4 +830,26 @@
       job.AddInstance(*it);
     }
   }
+
+
+  void ServerContext::SignalUpdatedModalities()
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins())
+    {
+      GetPlugins().SignalUpdatedModalities();
+    }
+#endif
+  }
+
+   
+  void ServerContext::SignalUpdatedPeers()
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins())
+    {
+      GetPlugins().SignalUpdatedPeers();
+    }
+#endif
+  }
 }
--- a/OrthancServer/ServerContext.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerContext.h	Thu Dec 06 15:58:08 2018 +0100
@@ -160,12 +160,19 @@
     DicomCacheProvider provider_;
     boost::mutex dicomCacheMutex_;
     MemoryCache dicomCache_;
-    JobsEngine jobsEngine_;
 
     LuaScripting mainLua_;
     LuaScripting filterLua_;
     LuaServerListener  luaListener_;
-
+    std::auto_ptr<SharedArchive>  mediaArchive_;
+    
+    // The "JobsEngine" must be *after* "LuaScripting", as
+    // "LuaScripting" embeds "LuaJobManager" that registers as an
+    // observer to "SequenceOfOperationsJob", whose lifetime
+    // corresponds to that of "JobsEngine". It must also be after
+    // "mediaArchive_", as jobs might access this archive.
+    JobsEngine jobsEngine_;
+    
 #if ORTHANC_ENABLE_PLUGINS == 1
     OrthancPlugins* plugins_;
 #endif
@@ -180,7 +187,7 @@
     boost::thread  changeThread_;
     boost::thread  saveJobsThread_;
         
-    SharedArchive  queryRetrieveArchive_;
+    std::auto_ptr<SharedArchive>  queryRetrieveArchive_;
     std::string defaultLocalAet_;
     OrthancHttpHandler  httpHandler_;
 
@@ -206,7 +213,8 @@
 
     ServerContext(IDatabaseWrapper& database,
                   IStorageArea& area,
-                  bool unitTesting);
+                  bool unitTesting,
+                  size_t maxCompletedJobs);
 
     ~ServerContext();
 
@@ -301,7 +309,12 @@
 
     SharedArchive& GetQueryRetrieveArchive()
     {
-      return queryRetrieveArchive_;
+      return *queryRetrieveArchive_;
+    }
+
+    SharedArchive& GetMediaArchive()
+    {
+      return *mediaArchive_;
     }
 
     const std::string& GetDefaultLocalApplicationEntityTitle() const
@@ -346,5 +359,9 @@
 
     void AddChildInstances(SetOfInstancesJob& job,
                            const std::string& publicId);
+
+    void SignalUpdatedModalities();
+
+    void SignalUpdatedPeers();
   };
 }
--- a/OrthancServer/ServerEnumerations.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerEnumerations.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -176,13 +176,13 @@
     switch (type)
     {
       case FileContentType_Dicom:
-        return "application/dicom";
+        return EnumerationToString(MimeType_Dicom);
 
       case FileContentType_DicomAsJson:
-        return "application/json";
+        return MIME_JSON_UTF8;
 
       default:
-        return "application/octet-stream";
+        return EnumerationToString(MimeType_Binary);
     }
   }
 
--- a/OrthancServer/ServerEnumerations.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerEnumerations.h	Thu Dec 06 15:58:08 2018 +0100
@@ -93,8 +93,10 @@
     GlobalProperty_FlushSleep = 2,
     GlobalProperty_AnonymizationSequence = 3,
     GlobalProperty_JobsRegistry = 5,
-    GlobalProperty_TotalCompressedSize = 6,     // Reserved for Orthanc > 1.4.1
-    GlobalProperty_TotalUncompressedSize = 7,   // Reserved for Orthanc > 1.4.1
+    GlobalProperty_TotalCompressedSize = 6,     // Reserved for Orthanc > 1.4.3
+    GlobalProperty_TotalUncompressedSize = 7,   // Reserved for Orthanc > 1.4.3
+    GlobalProperty_Modalities = 20,             // New in Orthanc 1.4.3
+    GlobalProperty_Peers = 21,                  // New in Orthanc 1.4.3
 
     // Reserved values for internal use by the database plugins
     GlobalProperty_DatabasePatchLevel = 4,
--- a/OrthancServer/ServerIndex.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerIndex.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -40,7 +40,7 @@
 
 #include "ServerIndexChange.h"
 #include "EmbeddedResources.h"
-#include "OrthancInitialization.h"
+#include "OrthancConfiguration.h"
 #include "../Core/DicomParsing/ParsedDicomFile.h"
 #include "ServerToolbox.h"
 #include "../Core/Toolbox.h"
@@ -621,8 +621,6 @@
 
     instanceMetadata.clear();
 
-    DicomInstanceHasher hasher(instanceToStore.GetSummary());
-
     try
     {
       Transaction t(*this);
@@ -631,14 +629,14 @@
       {
         ResourceType type;
         int64_t tmp;
-        if (db_.LookupResource(tmp, type, hasher.HashInstance()))
+        if (db_.LookupResource(tmp, type, instanceToStore.GetHasher().HashInstance()))
         {
           assert(type == ResourceType_Instance);
 
           if (overwrite_)
           {
             // Overwrite the old instance
-            LOG(INFO) << "Overwriting instance: " << hasher.HashInstance();
+            LOG(INFO) << "Overwriting instance: " << instanceToStore.GetHasher().HashInstance();
             db_.DeleteResource(tmp);
           }
           else
@@ -658,10 +656,10 @@
         instanceSize += it->GetCompressedSize();
       }
 
-      Recycle(instanceSize, hasher.HashPatient());
+      Recycle(instanceSize, instanceToStore.GetHasher().HashPatient());
 
       // Create the instance
-      int64_t instance = CreateResource(hasher.HashInstance(), ResourceType_Instance);
+      int64_t instance = CreateResource(instanceToStore.GetHasher().HashInstance(), ResourceType_Instance);
       ServerToolbox::StoreMainDicomTags(db_, instance, ResourceType_Instance, dicomSummary);
 
       // Detect up to which level the patient/study/series/instance
@@ -674,26 +672,26 @@
       {
         ResourceType dummy;
 
-        if (db_.LookupResource(series, dummy, hasher.HashSeries()))
+        if (db_.LookupResource(series, dummy, instanceToStore.GetHasher().HashSeries()))
         {
           assert(dummy == ResourceType_Series);
           // The patient, the study and the series already exist
 
-          bool ok = (db_.LookupResource(patient, dummy, hasher.HashPatient()) &&
-                     db_.LookupResource(study, dummy, hasher.HashStudy()));
+          bool ok = (db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient()) &&
+                     db_.LookupResource(study, dummy, instanceToStore.GetHasher().HashStudy()));
           assert(ok);
         }
-        else if (db_.LookupResource(study, dummy, hasher.HashStudy()))
+        else if (db_.LookupResource(study, dummy, instanceToStore.GetHasher().HashStudy()))
         {
           assert(dummy == ResourceType_Study);
 
           // New series: The patient and the study already exist
           isNewSeries = true;
 
-          bool ok = db_.LookupResource(patient, dummy, hasher.HashPatient());
+          bool ok = db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient());
           assert(ok);
         }
-        else if (db_.LookupResource(patient, dummy, hasher.HashPatient()))
+        else if (db_.LookupResource(patient, dummy, instanceToStore.GetHasher().HashPatient()))
         {
           assert(dummy == ResourceType_Patient);
 
@@ -713,21 +711,21 @@
       // Create the series if needed
       if (isNewSeries)
       {
-        series = CreateResource(hasher.HashSeries(), ResourceType_Series);
+        series = CreateResource(instanceToStore.GetHasher().HashSeries(), ResourceType_Series);
         ServerToolbox::StoreMainDicomTags(db_, series, ResourceType_Series, dicomSummary);
       }
 
       // Create the study if needed
       if (isNewStudy)
       {
-        study = CreateResource(hasher.HashStudy(), ResourceType_Study);
+        study = CreateResource(instanceToStore.GetHasher().HashStudy(), ResourceType_Study);
         ServerToolbox::StoreMainDicomTags(db_, study, ResourceType_Study, dicomSummary);
       }
 
       // Create the patient if needed
       if (isNewPatient)
       {
-        patient = CreateResource(hasher.HashPatient(), ResourceType_Patient);
+        patient = CreateResource(instanceToStore.GetHasher().HashPatient(), ResourceType_Patient);
         ServerToolbox::StoreMainDicomTags(db_, patient, ResourceType_Patient, dicomSummary);
       }
 
@@ -853,13 +851,13 @@
       SeriesStatus seriesStatus = GetSeriesStatus(series);
       if (seriesStatus == SeriesStatus_Complete)
       {
-        LogChange(series, ChangeType_CompletedSeries, ResourceType_Series, hasher.HashSeries());
+        LogChange(series, ChangeType_CompletedSeries, ResourceType_Series, instanceToStore.GetHasher().HashSeries());
       }
 
       // Mark the parent resources of this instance as unstable
-      MarkAsUnstable(series, ResourceType_Series, hasher.HashSeries());
-      MarkAsUnstable(study, ResourceType_Study, hasher.HashStudy());
-      MarkAsUnstable(patient, ResourceType_Patient, hasher.HashPatient());
+      MarkAsUnstable(series, ResourceType_Series, instanceToStore.GetHasher().HashSeries());
+      MarkAsUnstable(study, ResourceType_Study, instanceToStore.GetHasher().HashStudy());
+      MarkAsUnstable(patient, ResourceType_Patient, instanceToStore.GetHasher().HashPatient());
 
       t.Commit(instanceSize);
 
@@ -1933,7 +1931,13 @@
   void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
                                                    unsigned int threadSleep)
   {
-    int stableAge = Configuration::GetGlobalUnsignedIntegerParameter("StableAge", 60);
+    int stableAge;
+    
+    {
+      OrthancConfiguration::ReaderLock lock;
+      stableAge = lock.GetConfiguration().GetUnsignedIntegerParameter("StableAge", 60);
+    }
+
     if (stableAge <= 0)
     {
       stableAge = 60;
--- a/OrthancServer/ServerIndex.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerIndex.h	Thu Dec 06 15:58:08 2018 +0100
@@ -38,7 +38,6 @@
 #include "../Core/Cache/LeastRecentlyUsedIndex.h"
 #include "../Core/SQLite/Connection.h"
 #include "../Core/DicomFormat/DicomMap.h"
-#include "../Core/DicomFormat/DicomInstanceHasher.h"
 #include "ServerEnumerations.h"
 
 #include "IDatabaseWrapper.h"
--- a/OrthancServer/ServerJobs/ArchiveJob.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerJobs/ArchiveJob.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -47,7 +47,12 @@
 
 static const uint64_t MEGA_BYTES = 1024 * 1024;
 static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024;
-static const char* MEDIA_IMAGES_FOLDER = "IMAGES"; 
+
+static const char* const MEDIA_IMAGES_FOLDER = "IMAGES"; 
+static const char* const KEY_DESCRIPTION = "Description";
+static const char* const KEY_INSTANCES_COUNT = "InstancesCount";
+static const char* const KEY_UNCOMPRESSED_SIZE_MB = "UncompressedSizeMB";
+
 
 namespace Orthanc
 {
@@ -778,11 +783,9 @@
   };
 
 
-  ArchiveJob::ArchiveJob(boost::shared_ptr<TemporaryFile>& target,
-                         ServerContext& context,
+  ArchiveJob::ArchiveJob(ServerContext& context,
                          bool isMedia,
                          bool enableExtendedSopClass) :
-    target_(target),
     context_(context),
     archive_(new ArchiveIndex(ResourceType_Patient)),  // root
     isMedia_(isMedia),
@@ -791,52 +794,143 @@
     instancesCount_(0),
     uncompressedSize_(0)
   {
+  }
+
+  
+  ArchiveJob::~ArchiveJob()
+  {
+    if (!mediaArchiveId_.empty())
+    {
+      context_.GetMediaArchive().Remove(mediaArchiveId_);
+    }
+  }
+
+
+  void ArchiveJob::SetSynchronousTarget(boost::shared_ptr<TemporaryFile>& target)
+  {
     if (target.get() == NULL)
     {
       throw OrthancException(ErrorCode_NullPointer);
     }
+    else if (writer_.get() != NULL ||  // Already started
+             synchronousTarget_.get() != NULL ||
+             asynchronousTarget_.get() != NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      synchronousTarget_ = target;
+    }
   }
 
 
+  void ArchiveJob::SetDescription(const std::string& description)
+  {
+    if (writer_.get() != NULL)   // Already started
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      description_ = description;
+    }
+  }
+
+  
   void ArchiveJob::AddResource(const std::string& publicId)
   {
     if (writer_.get() != NULL)   // Already started
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
-        
-    ResourceIdentifiers resource(context_.GetIndex(), publicId);
-    archive_->Add(context_.GetIndex(), resource);
+    else
+    {
+      ResourceIdentifiers resource(context_.GetIndex(), publicId);
+      archive_->Add(context_.GetIndex(), resource);
+    }
   }
 
   
   void ArchiveJob::Reset()
   {
-    LOG(ERROR) << "Cannot resubmit the creation of an archive";
-    throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                           "Cannot resubmit the creation of an archive");
   }
 
   
   void ArchiveJob::Start()
   {
+    TemporaryFile* target = NULL;
+    
+    if (synchronousTarget_.get() == NULL)
+    {
+      asynchronousTarget_.reset(new TemporaryFile);
+      target = asynchronousTarget_.get();
+    }
+    else
+    {
+      target = synchronousTarget_.get();
+    }
+    
     if (writer_.get() != NULL)
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
 
-    writer_.reset(new ZipWriterIterator(*target_, context_, *archive_,
+    writer_.reset(new ZipWriterIterator(*target, context_, *archive_,
                                         isMedia_, enableExtendedSopClass_));
 
     instancesCount_ = writer_->GetInstancesCount();
     uncompressedSize_ = writer_->GetUncompressedSize();
   }
 
+
+
+  namespace
+  {
+    class DynamicTemporaryFile : public IDynamicObject
+    {
+    private:
+      std::auto_ptr<TemporaryFile>   file_;
+
+    public:
+      DynamicTemporaryFile(TemporaryFile* f) : file_(f)
+      {
+        if (f == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+      }
+
+      const TemporaryFile& GetFile() const
+      {
+        assert(file_.get() != NULL);
+        return *file_;
+      }
+    };
+  }
   
+
+  void ArchiveJob::FinalizeTarget()
+  {
+    writer_.reset();  // Flush all the results
+
+    if (asynchronousTarget_.get() != NULL)
+    {
+      // Asynchronous behavior: Move the resulting file into the media archive
+      mediaArchiveId_ = context_.GetMediaArchive().Add(
+        new DynamicTemporaryFile(asynchronousTarget_.release()));
+    }
+  }
+    
+
   JobStepResult ArchiveJob::Step()
   {
     assert(writer_.get() != NULL);
 
-    if (target_.unique())
+    if (synchronousTarget_.get() != NULL &&
+        synchronousTarget_.unique())
     {
       LOG(WARNING) << "A client has disconnected while creating an archive";
       return JobStepResult::Failure(ErrorCode_NetworkProtocol);          
@@ -844,7 +938,7 @@
         
     if (writer_->GetStepsCount() == 0)
     {
-      writer_.reset();  // Flush all the results
+      FinalizeTarget();
       return JobStepResult::Success();
     }
     else
@@ -855,7 +949,7 @@
 
       if (currentStep_ == writer_->GetStepsCount())
       {
-        writer_.reset();  // Flush all the results
+        FinalizeTarget();
         return JobStepResult::Success();
       }
       else
@@ -893,12 +987,41 @@
     }
   }
 
-    
+
   void ArchiveJob::GetPublicContent(Json::Value& value)
   {
-    value["Description"] = description_;
-    value["InstancesCount"] = instancesCount_;
-    value["UncompressedSizeMB"] =
+    value = Json::objectValue;
+    value[KEY_DESCRIPTION] = description_;
+    value[KEY_INSTANCES_COUNT] = instancesCount_;
+    value[KEY_UNCOMPRESSED_SIZE_MB] =
       static_cast<unsigned int>(uncompressedSize_ / MEGA_BYTES);
   }
+
+
+  bool ArchiveJob::GetOutput(std::string& output,
+                             MimeType& mime,
+                             const std::string& key)
+  {   
+    if (key == "archive" &&
+        !mediaArchiveId_.empty())
+    {
+      SharedArchive::Accessor accessor(context_.GetMediaArchive(), mediaArchiveId_);
+
+      if (accessor.IsValid())
+      {
+        const DynamicTemporaryFile& f = dynamic_cast<DynamicTemporaryFile&>(accessor.GetItem());
+        f.GetFile().Read(output);
+        mime = MimeType_Zip;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }    
+    else
+    {
+      return false;
+    }
+  }
 }
--- a/OrthancServer/ServerJobs/ArchiveJob.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerJobs/ArchiveJob.h	Thu Dec 06 15:58:08 2018 +0100
@@ -50,7 +50,8 @@
     class ZipCommands;
     class ZipWriterIterator;
     
-    boost::shared_ptr<TemporaryFile>      target_;
+    boost::shared_ptr<TemporaryFile>      synchronousTarget_;
+    std::auto_ptr<TemporaryFile>          asynchronousTarget_;
     ServerContext&                        context_;
     boost::shared_ptr<ArchiveIndex>       archive_;
     bool                                  isMedia_;
@@ -61,17 +62,20 @@
     size_t                                currentStep_;
     unsigned int                          instancesCount_;
     uint64_t                              uncompressedSize_;
+    std::string                           mediaArchiveId_;
 
+    void FinalizeTarget();
+    
   public:
-    ArchiveJob(boost::shared_ptr<TemporaryFile>& target,
-               ServerContext& context,
+    ArchiveJob(ServerContext& context,
                bool isMedia,
                bool enableExtendedSopClass);
+    
+    virtual ~ArchiveJob();
+    
+    void SetSynchronousTarget(boost::shared_ptr<TemporaryFile>& synchronousTarget);
 
-    void SetDescription(const std::string& description)
-    {
-      description_ = description;
-    }
+    void SetDescription(const std::string& description);
 
     const std::string& GetDescription() const
     {
@@ -100,5 +104,9 @@
     {
       return false;  // Cannot serialize this kind of job
     }
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key);
   };
 }
--- a/OrthancServer/ServerJobs/LuaJobManager.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerJobs/LuaJobManager.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -34,7 +34,7 @@
 #include "../PrecompiledHeadersServer.h"
 #include "LuaJobManager.h"
 
-#include "../OrthancInitialization.h"
+#include "../OrthancConfiguration.h"
 #include "../../Core/Logging.h"
 
 #include "../../Core/JobsEngine/Operations/LogJobOperation.h"
@@ -68,7 +68,11 @@
     priority_(0),
     trailingTimeout_(5000)
   {
-    dicomTimeout_ = Configuration::GetGlobalUnsignedIntegerParameter("DicomAssociationCloseDelay", 5);
+    {
+      OrthancConfiguration::ReaderLock lock;
+      dicomTimeout_ = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomAssociationCloseDelay", 5);
+    }
+
     LOG(INFO) << "Lua: DICOM associations will be closed after "
               << dicomTimeout_ << " seconds of inactivity";
   }
@@ -139,6 +143,7 @@
       // Need to create a new job, as the previous one is either
       // finished, or is getting too long
       that_.currentJob_ = new SequenceOfOperationsJob;
+      that_.currentJob_->Register(that_);
       that_.currentJob_->SetDescription("Lua");
 
       {
--- a/OrthancServer/ServerJobs/MergeStudyJob.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -61,8 +61,8 @@
   {
     if (study == targetStudy_)
     {
-      LOG(ERROR) << "Cannot merge a study into the same study: " << study;
-      throw OrthancException(ErrorCode_UnknownResource);
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "Cannot merge a study into the same study: " + study);
     }
     else
     {
@@ -183,8 +183,8 @@
     if (!context_.GetIndex().LookupResourceType(type, targetStudy) ||
         type != ResourceType_Study)
     {
-      LOG(ERROR) << "Cannot merge into an unknown study: " << targetStudy;
-      throw OrthancException(ErrorCode_UnknownResource);
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "Cannot merge into an unknown study: " + targetStudy);
     }
 
 
@@ -257,8 +257,8 @@
     }
     else if (!context_.GetIndex().LookupResourceType(level, studyOrSeries))
     {
-      LOG(ERROR) << "Cannot find this resource: " << studyOrSeries;
-      throw OrthancException(ErrorCode_UnknownResource);
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "Cannot find this resource: " + studyOrSeries);
     }
     else
     {
@@ -273,9 +273,10 @@
           break;
           
         default:
-          LOG(ERROR) << "This resource is neither a study, nor a series: "
-                     << studyOrSeries << " is a " << EnumerationToString(level);
-          throw OrthancException(ErrorCode_UnknownResource);
+          throw OrthancException(ErrorCode_UnknownResource,
+                                 "This resource is neither a study, nor a series: " +
+                                 studyOrSeries + " is a " +
+                                 std::string(EnumerationToString(level)));
       }
     }    
   }
@@ -291,14 +292,14 @@
     }
     else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study))
     {
-      LOG(ERROR) << "This resource is not a series: " << series;
-      throw OrthancException(ErrorCode_UnknownResource);
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "This resource is not a series: " + series);
     }
     else if (parent == targetStudy_)
     {
-      LOG(ERROR) << "Cannot merge series " << series
-                 << " into its parent study " << targetStudy_;
-      throw OrthancException(ErrorCode_UnknownResource);
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "Cannot merge series " + series +
+                             " into its parent study " + targetStudy_);
     }
     else
     {
@@ -318,8 +319,8 @@
     else if (!context_.GetIndex().LookupResourceType(actualLevel, study) ||
              actualLevel != ResourceType_Study)
     {
-      LOG(ERROR) << "This resource is not a study: " << study;
-      throw OrthancException(ErrorCode_UnknownResource);
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "This resource is not a study: " + study);
     }
     else
     {
--- a/OrthancServer/ServerJobs/ResourceModificationJob.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -134,8 +134,8 @@
     if (modification_.get() == NULL ||
         output_.get() == NULL)
     {
-      LOG(ERROR) << "No modification was provided for this job";
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No modification was provided for this job");
     }
 
       
@@ -212,8 +212,8 @@
     std::string modifiedInstance;
     if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success)
     {
-      LOG(ERROR) << "Error while storing a modified instance " << instance;
-      throw OrthancException(ErrorCode_CannotStoreInstance);
+      throw OrthancException(ErrorCode_CannotStoreInstance,
+                             "Error while storing a modified instance " + instance);
     }
 
     assert(modifiedInstance == modifiedHasher.HashInstance());
--- a/OrthancServer/ServerJobs/SplitStudyJob.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -43,10 +43,11 @@
   {
     if (allowedTags_.find(tag) == allowedTags_.end())
     {
-      LOG(ERROR) << "Cannot modify the following tag while splitting a study "
-                 << "(not in the patient/study modules): "
-                 << FromDcmtkBridge::GetTagName(tag, "") << " (" << tag.Format() << ")";
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Cannot modify the following tag while splitting a study "
+                             "(not in the patient/study modules): " +
+                             FromDcmtkBridge::GetTagName(tag, "") +
+                             " (" + tag.Format() + ")");
     }
   }
 
@@ -173,8 +174,8 @@
     if (!context_.GetIndex().LookupResourceType(type, sourceStudy) ||
         type != ResourceType_Study)
     {
-      LOG(ERROR) << "Cannot split unknown study: " << sourceStudy;
-      throw OrthancException(ErrorCode_UnknownResource);
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "Cannot split unknown study " + sourceStudy);
     }
   }
   
@@ -209,8 +210,8 @@
     else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study) ||
              parent != sourceStudy_)
     {
-      LOG(ERROR) << "This series does not belong to the study to be split: " << series;
-      throw OrthancException(ErrorCode_UnknownResource);
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "This series does not belong to the study to be split: " + series);
     }
     else
     {
--- a/OrthancServer/ServerToolbox.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/ServerToolbox.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -78,7 +78,10 @@
                       const Json::Value& source,
                       DicomToJsonFormat format)
     {
-      assert(source.isObject());
+      if (!source.isObject())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
 
       target = Json::objectValue;
       Json::Value::Members members = source.getMemberNames();
@@ -299,17 +302,19 @@
             tmp != level ||
             !FindOneChildInstance(instance, database, resource, level))
         {
-          LOG(ERROR) << "Cannot find an instance for " << EnumerationToString(level) 
-                     << " with identifier " << *it;
-          throw OrthancException(ErrorCode_InternalError);
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Cannot find an instance for " +
+                                 std::string(EnumerationToString(level)) +
+                                 " with identifier " + *it);
         }
 
         // Get the DICOM file attached to some instances in the resource
         FileInfo attachment;
         if (!database.LookupAttachment(attachment, instance, FileContentType_Dicom))
         {
-          LOG(ERROR) << "Cannot retrieve the DICOM file associated with instance " << database.GetPublicId(instance);
-          throw OrthancException(ErrorCode_InternalError);
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Cannot retrieve the DICOM file associated with instance " +
+                                 database.GetPublicId(instance));
         }
 
         try
--- a/OrthancServer/SliceOrdering.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/SliceOrdering.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -407,8 +407,8 @@
     if (!SortUsingPositions() &&
         !SortUsingIndexInSeries())
     {
-      LOG(ERROR) << "Unable to order the slices of the series " << seriesId;
-      throw OrthancException(ErrorCode_CannotOrderSlices);
+      throw OrthancException(ErrorCode_CannotOrderSlices,
+                             "Unable to order the slices of series " + seriesId);
     }
   }
 
--- a/OrthancServer/main.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/OrthancServer/main.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -36,19 +36,21 @@
 
 #include <boost/algorithm/string/predicate.hpp>
 
-#include "../Core/Logging.h"
-#include "../Core/HttpServer/EmbeddedResourceHttpHandler.h"
-#include "../Core/HttpServer/FilesystemHttpHandler.h"
-#include "../Core/Lua/LuaFunctionCall.h"
 #include "../Core/DicomFormat/DicomArray.h"
 #include "../Core/DicomNetworking/DicomServer.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/HttpServer/EmbeddedResourceHttpHandler.h"
+#include "../Core/HttpServer/FilesystemHttpHandler.h"
+#include "../Core/HttpServer/MongooseServer.h"
+#include "../Core/Logging.h"
+#include "../Core/Lua/LuaFunctionCall.h"
+#include "../Plugins/Engine/OrthancPlugins.h"
+#include "OrthancConfiguration.h"
+#include "OrthancFindRequestHandler.h"
 #include "OrthancInitialization.h"
-#include "ServerContext.h"
-#include "OrthancFindRequestHandler.h"
 #include "OrthancMoveRequestHandler.h"
+#include "ServerContext.h"
 #include "ServerToolbox.h"
-#include "../Plugins/Engine/OrthancPlugins.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
 
 using namespace Orthanc;
 
@@ -89,19 +91,21 @@
 
 
 
-class ModalitiesFromConfiguration : public Orthanc::DicomServer::IRemoteModalities
+class ModalitiesFromConfiguration : public DicomServer::IRemoteModalities
 {
 public:
   virtual bool IsSameAETitle(const std::string& aet1,
                              const std::string& aet2) 
   {
-    return Orthanc::Configuration::IsSameAETitle(aet1, aet2);
+    OrthancConfiguration::ReaderLock lock;
+    return lock.GetConfiguration().IsSameAETitle(aet1, aet2);
   }
 
   virtual bool LookupAETitle(RemoteModalityParameters& modality,
                              const std::string& aet) 
   {
-    return Orthanc::Configuration::LookupDicomModalityUsingAETitle(modality, aet);
+    OrthancConfiguration::ReaderLock lock;
+    return lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, aet);
   }
 };
 
@@ -128,8 +132,11 @@
   {
     std::auto_ptr<OrthancFindRequestHandler> result(new OrthancFindRequestHandler(context_));
 
-    result->SetMaxResults(Configuration::GetGlobalUnsignedIntegerParameter("LimitFindResults", 0));
-    result->SetMaxInstances(Configuration::GetGlobalUnsignedIntegerParameter("LimitFindInstances", 0));
+    {
+      OrthancConfiguration::ReaderLock lock;
+      result->SetMaxResults(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0));
+      result->SetMaxInstances(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0));
+    }
 
     if (result->GetMaxResults() == 0)
     {
@@ -176,8 +183,9 @@
   OrthancApplicationEntityFilter(ServerContext& context) :
     context_(context)
   {
-    alwaysAllowEcho_ = Configuration::GetGlobalBoolParameter("DicomAlwaysAllowEcho", true);
-    alwaysAllowStore_ = Configuration::GetGlobalBoolParameter("DicomAlwaysAllowStore", true);
+    OrthancConfiguration::ReaderLock lock;
+    alwaysAllowEcho_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowEcho", true);
+    alwaysAllowStore_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowStore", true);
   }
 
   virtual bool IsAllowedConnection(const std::string& remoteIp,
@@ -187,9 +195,16 @@
     LOG(INFO) << "Incoming connection from AET " << remoteAet
               << " on IP " << remoteIp << ", calling AET " << calledAet;
 
-    return (alwaysAllowEcho_ ||
-            alwaysAllowStore_ ||
-            Configuration::IsKnownAETitle(remoteAet, remoteIp));
+    if (alwaysAllowEcho_ ||
+        alwaysAllowStore_)
+    {
+      return true;
+    }
+    else
+    {
+      OrthancConfiguration::ReaderLock lock;
+      return lock.GetConfiguration().IsKnownAETitle(remoteAet, remoteIp);
+    }
   }
 
   virtual bool IsAllowedRequest(const std::string& remoteIp,
@@ -197,7 +212,7 @@
                                 const std::string& calledAet,
                                 DicomRequestType type)
   {
-    LOG(INFO) << "Incoming " << Orthanc::EnumerationToString(type) << " request from AET "
+    LOG(INFO) << "Incoming " << EnumerationToString(type) << " request from AET "
               << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet;
     
     if (type == DicomRequestType_Echo &&
@@ -214,9 +229,10 @@
     }
     else
     {
+      OrthancConfiguration::ReaderLock lock;
+
       RemoteModalityParameters modality;
-    
-      if (Configuration::LookupDicomModalityUsingAETitle(modality, remoteAet))
+      if (lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, remoteAet))
       {
         return modality.IsRequestAllowed(type);
       }
@@ -283,7 +299,10 @@
       }
     }
 
-    return Configuration::GetGlobalBoolParameter(configuration, true);
+    {
+      OrthancConfiguration::ReaderLock lock;
+      return lock.GetConfiguration().GetBooleanParameter(configuration, true);
+    }
   }
 
 
@@ -308,7 +327,10 @@
       }
     }
 
-    return Configuration::GetGlobalBoolParameter(configuration, false);
+    {
+      OrthancConfiguration::ReaderLock lock;
+      return lock.GetConfiguration().GetBooleanParameter(configuration, false);
+    }
   }
 };
 
@@ -460,6 +482,11 @@
       message["OrthancError"] = EnumerationToString(errorCode);
       message["OrthancStatus"] = errorCode;
 
+      if (exception.HasDetails())
+      {
+        message["Details"] = exception.GetDetails();
+      }
+
       std::string info = message.toStyledString();
       output.SendStatus(httpStatus, info);
     }
@@ -652,11 +679,22 @@
 static void LoadPlugins(OrthancPlugins& plugins)
 {
   std::list<std::string> path;
-  Configuration::GetGlobalListOfStringsParameter(path, "Plugins");
+
+  {
+    OrthancConfiguration::ReaderLock lock;
+    lock.GetConfiguration().GetListOfStringsParameter(path, "Plugins");
+  }
+
   for (std::list<std::string>::const_iterator
          it = path.begin(); it != path.end(); ++it)
   {
-    std::string path = Configuration::InterpretStringParameterAsPath(*it);
+    std::string path;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      path = lock.GetConfiguration().InterpretStringParameterAsPath(*it);
+    }
+
     LOG(WARNING) << "Loading plugin(s) from: " << path;
     plugins.GetManager().RegisterPlugin(path);
   }  
@@ -693,7 +731,8 @@
     {
       // Handling of SIGHUP
 
-      if (Configuration::HasConfigurationChanged())
+      OrthancConfiguration::ReaderLock lock;
+      if (lock.GetConfiguration().HasConfigurationChanged())
       {
         LOG(WARNING) << "A SIGHUP signal has been received, resetting Orthanc";
         Logging::Flush();
@@ -702,7 +741,8 @@
       }
       else
       {
-        LOG(WARNING) << "A SIGHUP signal has been received, but is ignored as the configuration has not changed";
+        LOG(WARNING) << "A SIGHUP signal has been received, but is ignored "
+                     << "as the configuration has not changed on the disk";
         Logging::Flush();
         continue;
       }
@@ -740,57 +780,74 @@
                             OrthancRestApi& restApi,
                             OrthancPlugins* plugins)
 {
-  if (!Configuration::GetGlobalBoolParameter("HttpServerEnabled", true))
+  bool httpServerEnabled;
+
+  {
+    OrthancConfiguration::ReaderLock lock;
+    httpServerEnabled = lock.GetConfiguration().GetBooleanParameter("HttpServerEnabled", true);
+  }
+
+  if (!httpServerEnabled)
   {
     LOG(WARNING) << "The HTTP server is disabled";
     return WaitForExit(context, restApi);
   }
-
-  MyHttpExceptionFormatter exceptionFormatter(Configuration::GetGlobalBoolParameter("HttpDescribeErrors", true), plugins);
-  
-
-  // HTTP server
-  MyIncomingHttpRequestFilter httpFilter(context, plugins);
-  MongooseServer httpServer;
-  httpServer.SetPortNumber(Configuration::GetGlobalUnsignedIntegerParameter("HttpPort", 8042));
-  httpServer.SetRemoteAccessAllowed(Configuration::GetGlobalBoolParameter("RemoteAccessAllowed", false));
-  httpServer.SetKeepAliveEnabled(Configuration::GetGlobalBoolParameter("KeepAlive", false));
-  httpServer.SetHttpCompressionEnabled(Configuration::GetGlobalBoolParameter("HttpCompressionEnabled", true));
-  httpServer.SetIncomingHttpRequestFilter(httpFilter);
-  httpServer.SetHttpExceptionFormatter(exceptionFormatter);
-
-  httpServer.SetAuthenticationEnabled(Configuration::GetGlobalBoolParameter("AuthenticationEnabled", false));
-  Configuration::SetupRegisteredUsers(httpServer);
-
-  if (Configuration::GetGlobalBoolParameter("SslEnabled", false))
-  {
-    std::string certificate = Configuration::InterpretStringParameterAsPath(
-      Configuration::GetGlobalStringParameter("SslCertificate", "certificate.pem"));
-    httpServer.SetSslEnabled(true);
-    httpServer.SetSslCertificate(certificate.c_str());
-  }
   else
   {
-    httpServer.SetSslEnabled(false);
-  }
+    MyIncomingHttpRequestFilter httpFilter(context, plugins);
+    MongooseServer httpServer;
+    bool httpDescribeErrors;
 
-  httpServer.Register(context.GetHttpHandler());
+    {
+      OrthancConfiguration::ReaderLock lock;
+      
+      httpDescribeErrors = lock.GetConfiguration().GetBooleanParameter("HttpDescribeErrors", true);
+  
+      // HTTP server
+      //httpServer.SetThreadsCount(50);
+      httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042));
+      httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false));
+      httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", false));
+      httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", true));
+      httpServer.SetAuthenticationEnabled(lock.GetConfiguration().GetBooleanParameter("AuthenticationEnabled", false));
+
+      lock.GetConfiguration().SetupRegisteredUsers(httpServer);
 
-  if (httpServer.GetPortNumber() < 1024)
-  {
-    LOG(WARNING) << "The HTTP port is privileged (" 
-                 << httpServer.GetPortNumber() << " is below 1024), "
-                 << "make sure you run Orthanc as root/administrator";
-  }
+      if (lock.GetConfiguration().GetBooleanParameter("SslEnabled", false))
+      {
+        std::string certificate = lock.GetConfiguration().InterpretStringParameterAsPath(
+          lock.GetConfiguration().GetStringParameter("SslCertificate", "certificate.pem"));
+        httpServer.SetSslEnabled(true);
+        httpServer.SetSslCertificate(certificate.c_str());
+      }
+      else
+      {
+        httpServer.SetSslEnabled(false);
+      }
+    }
 
-  httpServer.Start();
-  
-  bool restart = WaitForExit(context, restApi);
+    MyHttpExceptionFormatter exceptionFormatter(httpDescribeErrors, plugins);
+        
+    httpServer.SetIncomingHttpRequestFilter(httpFilter);
+    httpServer.SetHttpExceptionFormatter(exceptionFormatter);
+    httpServer.Register(context.GetHttpHandler());
 
-  httpServer.Stop();
-  LOG(WARNING) << "    HTTP server has stopped";
+    if (httpServer.GetPortNumber() < 1024)
+    {
+      LOG(WARNING) << "The HTTP port is privileged (" 
+                   << httpServer.GetPortNumber() << " is below 1024), "
+                   << "make sure you run Orthanc as root/administrator";
+    }
 
-  return restart;
+    httpServer.Start();
+  
+    bool restart = WaitForExit(context, restApi);
+
+    httpServer.Stop();
+    LOG(WARNING) << "    HTTP server has stopped";
+
+    return restart;
+  }
 }
 
 
@@ -798,84 +855,96 @@
                              OrthancRestApi& restApi,
                              OrthancPlugins* plugins)
 {
-  if (!Configuration::GetGlobalBoolParameter("DicomServerEnabled", true))
+  bool dicomServerEnabled;
+
+  {
+    OrthancConfiguration::ReaderLock lock;
+    dicomServerEnabled = lock.GetConfiguration().GetBooleanParameter("DicomServerEnabled", true);
+  }
+
+  if (!dicomServerEnabled)
   {
     LOG(WARNING) << "The DICOM server is disabled";
     return StartHttpServer(context, restApi, plugins);
   }
-
-  MyDicomServerFactory serverFactory(context);
-  OrthancApplicationEntityFilter dicomFilter(context);
-  ModalitiesFromConfiguration modalities;
+  else
+  {
+    MyDicomServerFactory serverFactory(context);
+    OrthancApplicationEntityFilter dicomFilter(context);
+    ModalitiesFromConfiguration modalities;
   
-  // Setup the DICOM server  
-  DicomServer dicomServer;
-  dicomServer.SetRemoteModalities(modalities);
-  dicomServer.SetCalledApplicationEntityTitleCheck(Configuration::GetGlobalBoolParameter("DicomCheckCalledAet", false));
-  dicomServer.SetStoreRequestHandlerFactory(serverFactory);
-  dicomServer.SetMoveRequestHandlerFactory(serverFactory);
-  dicomServer.SetFindRequestHandlerFactory(serverFactory);
-  dicomServer.SetAssociationTimeout(Configuration::GetGlobalUnsignedIntegerParameter("DicomScpTimeout", 30));
+    // Setup the DICOM server  
+    DicomServer dicomServer;
+    dicomServer.SetRemoteModalities(modalities);
+    dicomServer.SetStoreRequestHandlerFactory(serverFactory);
+    dicomServer.SetMoveRequestHandlerFactory(serverFactory);
+    dicomServer.SetFindRequestHandlerFactory(serverFactory);
 
+    {
+      OrthancConfiguration::ReaderLock lock;
+      dicomServer.SetCalledApplicationEntityTitleCheck(lock.GetConfiguration().GetBooleanParameter("DicomCheckCalledAet", false));
+      dicomServer.SetAssociationTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScpTimeout", 30));
+      dicomServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242));
+      dicomServer.SetApplicationEntityTitle(lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"));
+    }
 
 #if ORTHANC_ENABLE_PLUGINS == 1
-  if (plugins != NULL)
-  {
-    if (plugins->HasWorklistHandler())
-    {
-      dicomServer.SetWorklistRequestHandlerFactory(*plugins);
-    }
-
-    if (plugins->HasFindHandler())
+    if (plugins != NULL)
     {
-      dicomServer.SetFindRequestHandlerFactory(*plugins);
-    }
+      if (plugins->HasWorklistHandler())
+      {
+        dicomServer.SetWorklistRequestHandlerFactory(*plugins);
+      }
 
-    if (plugins->HasMoveHandler())
-    {
-      dicomServer.SetMoveRequestHandlerFactory(*plugins);
+      if (plugins->HasFindHandler())
+      {
+        dicomServer.SetFindRequestHandlerFactory(*plugins);
+      }
+
+      if (plugins->HasMoveHandler())
+      {
+        dicomServer.SetMoveRequestHandlerFactory(*plugins);
+      }
     }
-  }
 #endif
 
-  dicomServer.SetPortNumber(Configuration::GetGlobalUnsignedIntegerParameter("DicomPort", 4242));
-  dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"));
-  dicomServer.SetApplicationEntityFilter(dicomFilter);
+    dicomServer.SetApplicationEntityFilter(dicomFilter);
 
-  if (dicomServer.GetPortNumber() < 1024)
-  {
-    LOG(WARNING) << "The DICOM port is privileged (" 
-                 << dicomServer.GetPortNumber() << " is below 1024), "
-                 << "make sure you run Orthanc as root/administrator";
-  }
+    if (dicomServer.GetPortNumber() < 1024)
+    {
+      LOG(WARNING) << "The DICOM port is privileged (" 
+                   << dicomServer.GetPortNumber() << " is below 1024), "
+                   << "make sure you run Orthanc as root/administrator";
+    }
 
-  dicomServer.Start();
-  LOG(WARNING) << "DICOM server listening with AET " << dicomServer.GetApplicationEntityTitle() 
-               << " on port: " << dicomServer.GetPortNumber();
+    dicomServer.Start();
+    LOG(WARNING) << "DICOM server listening with AET " << dicomServer.GetApplicationEntityTitle() 
+                 << " on port: " << dicomServer.GetPortNumber();
 
-  bool restart = false;
-  ErrorCode error = ErrorCode_Success;
+    bool restart = false;
+    ErrorCode error = ErrorCode_Success;
 
-  try
-  {
-    restart = StartHttpServer(context, restApi, plugins);
-  }
-  catch (OrthancException& e)
-  {
-    error = e.GetErrorCode();
-  }
+    try
+    {
+      restart = StartHttpServer(context, restApi, plugins);
+    }
+    catch (OrthancException& e)
+    {
+      error = e.GetErrorCode();
+    }
 
-  dicomServer.Stop();
-  LOG(WARNING) << "    DICOM server has stopped";
+    dicomServer.Stop();
+    LOG(WARNING) << "    DICOM server has stopped";
 
-  serverFactory.Done();
+    serverFactory.Done();
 
-  if (error != ErrorCode_Success)
-  {
-    throw OrthancException(error);
+    if (error != ErrorCode_Success)
+    {
+      throw OrthancException(error);
+    }
+
+    return restart;
   }
-
-  return restart;
 }
 
 
@@ -934,9 +1003,10 @@
 
   if (currentVersion > ORTHANC_DATABASE_VERSION)
   {
-    LOG(ERROR) << "The version of the database schema (" << currentVersion
-               << ") is too recent for this version of Orthanc. Please upgrade Orthanc.";
-    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
+                           "The version of the database schema (" +
+                           boost::lexical_cast<std::string>(currentVersion) +
+                           ") is too recent for this version of Orthanc. Please upgrade Orthanc.");
   }
 
   LOG(WARNING) << "Upgrading the database from schema version "
@@ -957,8 +1027,9 @@
   currentVersion = database.GetDatabaseVersion();
   if (ORTHANC_DATABASE_VERSION != currentVersion)
   {
-    LOG(ERROR) << "The database schema was not properly upgraded, it is still at version " << currentVersion;
-    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
+                           "The database schema was not properly upgraded, it is still at version " +
+                           boost::lexical_cast<std::string>(currentVersion));
   }
   else
   {
@@ -968,86 +1039,120 @@
 }
 
 
+
+namespace
+{
+  class ServerContextConfigurator : public boost::noncopyable
+  {
+  private:
+    ServerContext&   context_;
+    OrthancPlugins*  plugins_;
+
+  public:
+    ServerContextConfigurator(ServerContext& context,
+                              OrthancPlugins* plugins) :
+      context_(context),
+      plugins_(plugins)
+    {
+      {
+        OrthancConfiguration::WriterLock lock;
+        lock.GetConfiguration().SetServerIndex(context.GetIndex());
+      }
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+      if (plugins_ != NULL)
+      {
+        plugins_->SetServerContext(context_);
+        context_.SetPlugins(*plugins_);
+      }
+#endif
+    }
+
+    ~ServerContextConfigurator()
+    {
+      {
+        OrthancConfiguration::WriterLock lock;
+        lock.GetConfiguration().ResetServerIndex();
+      }
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+      if (plugins_ != NULL)
+      {
+        plugins_->ResetServerContext();
+        context_.ResetPlugins();
+      }
+#endif
+    }
+  };
+}
+
+
 static bool ConfigureServerContext(IDatabaseWrapper& database,
                                    IStorageArea& storageArea,
                                    OrthancPlugins *plugins,
                                    bool loadJobsFromDatabase)
 {
-  // These configuration options must be set before creating the
-  // ServerContext, otherwise the possible Lua scripts will not be
-  // able to properly issue HTTP/HTTPS queries
-  HttpClient::ConfigureSsl(Configuration::GetGlobalBoolParameter("HttpsVerifyPeers", true),
-                           Configuration::InterpretStringParameterAsPath
-                           (Configuration::GetGlobalStringParameter("HttpsCACertificates", "")));
-  HttpClient::SetDefaultVerbose(Configuration::GetGlobalBoolParameter("HttpVerbose", false));
-  HttpClient::SetDefaultTimeout(Configuration::GetGlobalUnsignedIntegerParameter("HttpTimeout", 0));
-  HttpClient::SetDefaultProxy(Configuration::GetGlobalStringParameter("HttpProxy", ""));
+  size_t maxCompletedJobs;
+  
+  {
+    OrthancConfiguration::ReaderLock lock;
 
-  DicomUserConnection::SetDefaultTimeout(Configuration::GetGlobalUnsignedIntegerParameter("DicomScuTimeout", 10));
+    // These configuration options must be set before creating the
+    // ServerContext, otherwise the possible Lua scripts will not be
+    // able to properly issue HTTP/HTTPS queries
+    HttpClient::ConfigureSsl(lock.GetConfiguration().GetBooleanParameter("HttpsVerifyPeers", true),
+                             lock.GetConfiguration().InterpretStringParameterAsPath
+                             (lock.GetConfiguration().GetStringParameter("HttpsCACertificates", "")));
+    HttpClient::SetDefaultVerbose(lock.GetConfiguration().GetBooleanParameter("HttpVerbose", false));
+    HttpClient::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpTimeout", 0));
+    HttpClient::SetDefaultProxy(lock.GetConfiguration().GetStringParameter("HttpProxy", ""));
+    
+    DicomUserConnection::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10));
 
-  ServerContext context(database, storageArea, false /* not running unit tests */);
-  context.SetCompressionEnabled(Configuration::GetGlobalBoolParameter("StorageCompression", false));
-  context.SetStoreMD5ForAttachments(Configuration::GetGlobalBoolParameter("StoreMD5ForAttachments", true));
+    maxCompletedJobs = lock.GetConfiguration().GetUnsignedIntegerParameter("JobsHistorySize", 10);
+  }
+  
+  ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs);
+
+  {
+    OrthancConfiguration::ReaderLock lock;
 
-  // New option in Orthanc 1.4.2
-  context.GetIndex().SetOverwriteInstances(Configuration::GetGlobalBoolParameter("OverwriteInstances", false));
+    context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
+    context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
+
+    // New option in Orthanc 1.4.2
+    context.GetIndex().SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
 
-  try
-  {
-    context.GetIndex().SetMaximumPatientCount(Configuration::GetGlobalUnsignedIntegerParameter("MaximumPatientCount", 0));
-  }
-  catch (...)
-  {
-    context.GetIndex().SetMaximumPatientCount(0);
-  }
+    try
+    {
+      context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
+    }
+    catch (...)
+    {
+      context.GetIndex().SetMaximumPatientCount(0);
+    }
 
-  try
-  {
-    uint64_t size = Configuration::GetGlobalUnsignedIntegerParameter("MaximumStorageSize", 0);
-    context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
-  }
-  catch (...)
-  {
-    context.GetIndex().SetMaximumStorageSize(0);
+    try
+    {
+      uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
+      context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
+    }
+    catch (...)
+    {
+      context.GetIndex().SetMaximumStorageSize(0);
+    }
   }
 
-  context.GetJobsEngine().GetRegistry().SetMaxCompletedJobs
-    (Configuration::GetGlobalUnsignedIntegerParameter("JobsHistorySize", 10));
+  {
+    ServerContextConfigurator configurator(context, plugins);
 
-#if ORTHANC_ENABLE_PLUGINS == 1
-  if (plugins)
-  {
-    plugins->SetServerContext(context);
-    context.SetPlugins(*plugins);
-  }
-#endif
+    {
+      OrthancConfiguration::WriterLock lock;
+      lock.GetConfiguration().LoadModalitiesAndPeers();
+    }
 
-  bool restart = false;
-  ErrorCode error = ErrorCode_Success;
-
-  try
-  {
-    restart = ConfigureHttpHandler(context, plugins, loadJobsFromDatabase);
+    return ConfigureHttpHandler(context, plugins, loadJobsFromDatabase);
   }
-  catch (OrthancException& e)
-  {
-    error = e.GetErrorCode();
-  }
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-  if (plugins)
-  {
-    plugins->ResetServerContext();
-    context.ResetPlugins();
-  }
-#endif
-
-  if (error != ErrorCode_Success)
-  {
-    throw OrthancException(error);
-  }
-
-  return restart;
 }
 
 
@@ -1068,10 +1173,11 @@
   }
   else if (currentVersion != ORTHANC_DATABASE_VERSION)
   {
-    LOG(ERROR) << "The database schema must be changed from version "
-               << currentVersion << " to " << ORTHANC_DATABASE_VERSION 
-               << ": Please run Orthanc with the \"--upgrade\" argument";
-    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
+                           "The database schema must be changed from version " +
+                           boost::lexical_cast<std::string>(currentVersion) + " to " +
+                           boost::lexical_cast<std::string>(ORTHANC_DATABASE_VERSION) +
+                           ": Please run Orthanc with the \"--upgrade\" argument");
   }
 
   bool success = ConfigureServerContext
@@ -1104,7 +1210,7 @@
   }
   else
   {
-    databasePtr.reset(Configuration::CreateDatabaseWrapper());
+    databasePtr.reset(CreateDatabaseWrapper());
     database = databasePtr.get();
   }
 
@@ -1115,7 +1221,7 @@
   }
   else
   {
-    storage.reset(Configuration::CreateStorageArea());
+    storage.reset(CreateStorageArea());
   }
 
   assert(database != NULL);
@@ -1126,8 +1232,8 @@
 
 #elif ORTHANC_ENABLE_PLUGINS == 0
   // The plugins are disabled
-  databasePtr.reset(Configuration::CreateDatabaseWrapper());
-  storage.reset(Configuration::CreateStorageArea());
+  databasePtr.reset(lock.GetConfiguration().CreateDatabaseWrapper());
+  storage.reset(lock.GetConfiguration().CreateStorageArea());
 
   return ConfigureDatabase(*databasePtr, *storage, NULL,
                            upgradeDatabase, loadJobsFromDatabase);
--- a/Plugins/Engine/OrthancPluginDatabase.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Engine/OrthancPluginDatabase.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -280,8 +280,8 @@
   {
     if (extensions_.getAllInternalIds == NULL)
     {
-      LOG(ERROR) << "The database plugin does not implement the GetAllInternalIds primitive";
-      throw OrthancException(ErrorCode_DatabasePlugin);
+      throw OrthancException(ErrorCode_DatabasePlugin,
+                             "The database plugin does not implement the GetAllInternalIds primitive");
     }
 
     ResetAnswers();
@@ -618,8 +618,8 @@
   {
     if (extensions_.lookupIdentifier3 == NULL)
     {
-      LOG(ERROR) << "The database plugin does not implement the LookupIdentifier3 primitive";
-      throw OrthancException(ErrorCode_DatabasePlugin);
+      throw OrthancException(ErrorCode_DatabasePlugin,
+                             "The database plugin does not implement the LookupIdentifier3 primitive");
     }
 
     OrthancPluginDicomTag tmp;
@@ -736,8 +736,8 @@
   {
     if (extensions_.clearMainDicomTags == NULL)
     {
-      LOG(ERROR) << "Your custom index plugin does not implement the ClearMainDicomTags() extension";
-      throw OrthancException(ErrorCode_DatabasePlugin);
+      throw OrthancException(ErrorCode_DatabasePlugin,
+                             "Your custom index plugin does not implement the ClearMainDicomTags() extension");
     }
 
     CheckSuccess(extensions_.clearMainDicomTags(payload_, id));
@@ -971,14 +971,15 @@
           break;
 
         default:
-          LOG(ERROR) << "Unhandled type of answer for custom index plugin: " << answer.type;
-          throw OrthancException(ErrorCode_DatabasePlugin);
+          throw OrthancException(ErrorCode_DatabasePlugin,
+                                 "Unhandled type of answer for custom index plugin: " +
+                                 boost::lexical_cast<std::string>(answer.type));
       }
     }
     else if (type_ != answer.type)
     {
-      LOG(ERROR) << "Error in the plugin protocol: Cannot change the answer type";
-      throw OrthancException(ErrorCode_DatabasePlugin);
+      throw OrthancException(ErrorCode_DatabasePlugin,
+                             "Error in the plugin protocol: Cannot change the answer type");
     }
 
     switch (answer.type)
@@ -1098,8 +1099,9 @@
       }
 
       default:
-        LOG(ERROR) << "Unhandled type of answer for custom index plugin: " << answer.type;
-        throw OrthancException(ErrorCode_DatabasePlugin);
+        throw OrthancException(ErrorCode_DatabasePlugin,
+                               "Unhandled type of answer for custom index plugin: " +
+                               boost::lexical_cast<std::string>(answer.type));
     }
   }
 }
--- a/Plugins/Engine/OrthancPlugins.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -48,7 +48,7 @@
 #include "../../Core/Toolbox.h"
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/DicomParsing/ToDcmtkBridge.h"
-#include "../../OrthancServer/OrthancInitialization.h"
+#include "../../OrthancServer/OrthancConfiguration.h"
 #include "../../OrthancServer/ServerContext.h"
 #include "../../OrthancServer/ServerToolbox.h"
 #include "../../OrthancServer/Search/HierarchicalMatcher.h"
@@ -271,8 +271,10 @@
     public:
       OrthancPeers()
       {
+        OrthancConfiguration::ReaderLock lock;
+
         std::set<std::string> peers;
-        Configuration::GetListOfOrthancPeers(peers);
+        lock.GetConfiguration().GetListOfOrthancPeers(peers);
 
         names_.reserve(peers.size());
         parameters_.reserve(peers.size());
@@ -281,7 +283,7 @@
                it = peers.begin(); it != peers.end(); ++it)
         {
           WebServiceParameters peer;
-          if (Configuration::GetOrthancPeer(peer, *it))
+          if (lock.GetConfiguration().LookupOrthancPeer(peer, *it))
           {
             names_.push_back(*it);
             parameters_.push_back(peer);
@@ -317,6 +319,56 @@
     
 
   public:
+    class PluginHttpOutput : public boost::noncopyable
+    {
+    private:
+      HttpOutput&                 output_;
+      std::auto_ptr<std::string>  errorDetails_;
+      bool                        logDetails_;
+
+    public:
+      PluginHttpOutput(HttpOutput& output) :
+        output_(output),
+        logDetails_(false)
+      {
+      }
+
+      HttpOutput& GetOutput()
+      {
+        return output_;
+      }
+
+      void SetErrorDetails(const std::string& details,
+                           bool logDetails)
+      {
+        errorDetails_.reset(new std::string(details));
+        logDetails_ = logDetails;
+      }
+
+      bool HasErrorDetails() const
+      {
+        return errorDetails_.get() != NULL;
+      }
+
+      bool IsLogDetails() const
+      {
+        return logDetails_;
+      }
+
+      const std::string& GetErrorDetails() const
+      {
+        if (errorDetails_.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+        else
+        {
+          return *errorDetails_;
+        }
+      }
+    };
+
+    
     class RestCallback : public boost::noncopyable
     {
     private:
@@ -324,7 +376,7 @@
       OrthancPluginRestCallback callback_;
       bool                      lock_;
 
-      OrthancPluginErrorCode InvokeInternal(HttpOutput& output,
+      OrthancPluginErrorCode InvokeInternal(PluginHttpOutput& output,
                                             const std::string& flatUri,
                                             const OrthancPluginHttpRequest& request)
       {
@@ -349,7 +401,7 @@
       }
 
       OrthancPluginErrorCode Invoke(boost::recursive_mutex& restCallbackMutex,
-                                    HttpOutput& output,
+                                    PluginHttpOutput& output,
                                     const std::string& flatUri,
                                     const OrthancPluginHttpRequest& request)
       {
@@ -731,7 +783,8 @@
           OrthancPluginErrorCode error = apply_(driver_);
           if (error != OrthancPluginErrorCode_Success)
           {
-            LOG(ERROR) << "Error while doing C-Move from plugin: " << EnumerationToString(static_cast<ErrorCode>(error));
+            LOG(ERROR) << "Error while doing C-Move from plugin: "
+                       << EnumerationToString(static_cast<ErrorCode>(error));
             return Status_Failure;
           }
           else
@@ -814,8 +867,8 @@
 
       if (driver == NULL)
       {
-        LOG(ERROR) << "Plugin cannot create a driver for an incoming C-MOVE request";
-        throw OrthancException(ErrorCode_Plugin);
+        throw OrthancException(ErrorCode_Plugin,
+                               "Plugin cannot create a driver for an incoming C-MOVE request");
       }
 
       unsigned int size = params_.getMoveSize(driver);
@@ -1022,7 +1075,11 @@
     }
 
     assert(callback != NULL);
-    OrthancPluginErrorCode error = callback->Invoke(pimpl_->restCallbackMutex_, output, flatUri, request);
+
+    PImpl::PluginHttpOutput pluginOutput(output);
+
+    OrthancPluginErrorCode error = callback->Invoke
+      (pimpl_->restCallbackMutex_, pluginOutput, flatUri, request);
 
     if (error == OrthancPluginErrorCode_Success && 
         output.IsWritingMultipart())
@@ -1037,7 +1094,17 @@
     else
     {
       GetErrorDictionary().LogError(error, false);
-      throw OrthancException(static_cast<ErrorCode>(error));
+
+      if (pluginOutput.HasErrorDetails())
+      {
+        throw OrthancException(static_cast<ErrorCode>(error),
+                               pluginOutput.GetErrorDetails(),
+                               pluginOutput.IsLogDetails());
+      }
+      else
+      {
+        throw OrthancException(static_cast<ErrorCode>(error));
+      }
     }
   }
 
@@ -1142,8 +1209,8 @@
 
     if (pimpl_->worklistCallback_ != NULL)
     {
-      LOG(ERROR) << "Can only register one plugin to handle modality worklists";
-      throw OrthancException(ErrorCode_Plugin);
+      throw OrthancException(ErrorCode_Plugin,
+                             "Can only register one plugin to handle modality worklists");
     }
     else
     {
@@ -1162,8 +1229,8 @@
 
     if (pimpl_->findCallback_ != NULL)
     {
-      LOG(ERROR) << "Can only register one plugin to handle C-FIND requests";
-      throw OrthancException(ErrorCode_Plugin);
+      throw OrthancException(ErrorCode_Plugin,
+                             "Can only register one plugin to handle C-FIND requests");
     }
     else
     {
@@ -1182,8 +1249,8 @@
 
     if (pimpl_->moveCallbacks_.callback != NULL)
     {
-      LOG(ERROR) << "Can only register one plugin to handle C-MOVE requests";
-      throw OrthancException(ErrorCode_Plugin);
+      throw OrthancException(ErrorCode_Plugin,
+                             "Can only register one plugin to handle C-MOVE requests");
     }
     else
     {
@@ -1244,9 +1311,9 @@
     const _OrthancPluginAnswerBuffer& p = 
       *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters);
 
-    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
-    translatedOutput->SetContentType(p.mimeType);
-    translatedOutput->Answer(p.answer, p.answerSize);
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.SetContentType(p.mimeType);
+    translatedOutput.Answer(p.answer, p.answerSize);
   }
 
 
@@ -1255,8 +1322,8 @@
     const _OrthancPluginOutputPlusArgument& p = 
       *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
 
-    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
-    translatedOutput->Redirect(p.argument);
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.Redirect(p.argument);
   }
 
 
@@ -1265,8 +1332,8 @@
     const _OrthancPluginSendHttpStatusCode& p = 
       *reinterpret_cast<const _OrthancPluginSendHttpStatusCode*>(parameters);
 
-    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
-    translatedOutput->SendStatus(static_cast<HttpStatus>(p.status));
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.SendStatus(static_cast<HttpStatus>(p.status));
   }
 
 
@@ -1275,16 +1342,16 @@
     const _OrthancPluginSendHttpStatus& p = 
       *reinterpret_cast<const _OrthancPluginSendHttpStatus*>(parameters);
 
-    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
     HttpStatus status = static_cast<HttpStatus>(p.status);
 
     if (p.bodySize > 0 && p.body != NULL)
     {
-      translatedOutput->SendStatus(status, p.body, p.bodySize);
+      translatedOutput.SendStatus(status, p.body, p.bodySize);
     }
     else
     {
-      translatedOutput->SendStatus(status);
+      translatedOutput.SendStatus(status);
     }
   }
 
@@ -1294,8 +1361,8 @@
     const _OrthancPluginOutputPlusArgument& p = 
       *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
 
-    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
-    translatedOutput->SendUnauthorized(p.argument);
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.SendUnauthorized(p.argument);
   }
 
 
@@ -1304,8 +1371,8 @@
     const _OrthancPluginOutputPlusArgument& p = 
       *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
 
-    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
-    translatedOutput->SendMethodNotAllowed(p.argument);
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.SendMethodNotAllowed(p.argument);
   }
 
 
@@ -1314,8 +1381,8 @@
     const _OrthancPluginSetHttpHeader& p = 
       *reinterpret_cast<const _OrthancPluginSetHttpHeader*>(parameters);
 
-    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
-    translatedOutput->SetCookie(p.key, p.value);
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.SetCookie(p.key, p.value);
   }
 
 
@@ -1324,8 +1391,19 @@
     const _OrthancPluginSetHttpHeader& p = 
       *reinterpret_cast<const _OrthancPluginSetHttpHeader*>(parameters);
 
-    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
-    translatedOutput->AddHeader(p.key, p.value);
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.AddHeader(p.key, p.value);
+  }
+
+
+  void OrthancPlugins::SetHttpErrorDetails(const void* parameters)
+  {
+    const _OrthancPluginSetHttpErrorDetails& p = 
+      *reinterpret_cast<const _OrthancPluginSetHttpErrorDetails*>(parameters);
+
+    PImpl::PluginHttpOutput* output =
+      reinterpret_cast<PImpl::PluginHttpOutput*>(p.output);
+    output->SetErrorDetails(p.details, p.log);
   }
 
 
@@ -1354,7 +1432,7 @@
     const _OrthancPluginCompressAndAnswerImage& p = 
       *reinterpret_cast<const _OrthancPluginCompressAndAnswerImage*>(parameters);
 
-    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
 
     ImageAccessor accessor;
     accessor.AssignReadOnly(Plugins::Convert(p.pixelFormat), p.width, p.height, p.pitch, p.buffer);
@@ -1367,7 +1445,7 @@
       {
         PngWriter writer;
         writer.WriteToMemory(compressed, accessor);
-        translatedOutput->SetContentType("image/png");
+        translatedOutput.SetContentType(MimeType_Png);
         break;
       }
 
@@ -1376,7 +1454,7 @@
         JpegWriter writer;
         writer.SetQuality(p.quality);
         writer.WriteToMemory(compressed, accessor);
-        translatedOutput->SetContentType("image/jpeg");
+        translatedOutput.SetContentType(MimeType_Jpeg);
         break;
       }
 
@@ -1384,7 +1462,7 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    translatedOutput->Answer(compressed);
+    translatedOutput.Answer(compressed);
   }
 
 
@@ -2105,19 +2183,23 @@
   {
     const _OrthancPluginGetFontInfo& p = *reinterpret_cast<const _OrthancPluginGetFontInfo*>(parameters);
 
-    const Font& font = Configuration::GetFontRegistry().GetFont(p.fontIndex);
-
-    if (p.name != NULL)
     {
-      *(p.name) = font.GetName().c_str();
-    }
-    else if (p.size != NULL)
-    {
-      *(p.size) = font.GetSize();
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_InternalError);
+      OrthancConfiguration::ReaderLock lock;
+
+      const Font& font = lock.GetConfiguration().GetFontRegistry().GetFont(p.fontIndex);
+
+      if (p.name != NULL)
+      {
+        *(p.name) = font.GetName().c_str();
+      }
+      else if (p.size != NULL)
+      {
+        *(p.size) = font.GetSize();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
     }
   }
 
@@ -2127,9 +2209,12 @@
     const _OrthancPluginDrawText& p = *reinterpret_cast<const _OrthancPluginDrawText*>(parameters);
 
     ImageAccessor& target = *reinterpret_cast<ImageAccessor*>(p.image);
-    const Font& font = Configuration::GetFontRegistry().GetFont(p.fontIndex);
-
-    font.Draw(target, p.utf8Text, p.x, p.y, p.r, p.g, p.b);
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      const Font& font = lock.GetConfiguration().GetFontRegistry().GetFont(p.fontIndex);
+      font.Draw(target, p.utf8Text, p.x, p.y, p.r, p.g, p.b);
+    }
   }
 
 
@@ -2275,10 +2360,10 @@
     const _OrthancPluginAnswerBuffer& p =
       *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters);
 
-    HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output);
+    HttpOutput& output = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
 
     std::map<std::string, std::string> headers;  // No custom headers
-    output->SendMultipartItem(p.answer, p.answerSize, headers);
+    output.SendMultipartItem(p.answer, p.answerSize, headers);
   }
 
 
@@ -2288,7 +2373,7 @@
     // connection was closed by the HTTP client.
     const _OrthancPluginSendMultipartItem2& p =
       *reinterpret_cast<const _OrthancPluginSendMultipartItem2*>(parameters);
-    HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output);
+    HttpOutput& output = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
     
     std::map<std::string, std::string> headers;
     for (uint32_t i = 0; i < p.headersCount; i++)
@@ -2296,7 +2381,7 @@
       headers[p.headersKeys[i]] = p.headersValues[i];
     }
     
-    output->SendMultipartItem(p.answer, p.answerSize, headers);
+    output.SendMultipartItem(p.answer, p.answerSize, headers);
   }
       
 
@@ -2311,8 +2396,8 @@
     }
     else
     {
-      LOG(ERROR) << "Cannot invoke this service without a custom database back-end";
-      throw OrthancException(ErrorCode_BadRequest);
+      throw OrthancException(ErrorCode_BadRequest,
+                             "Cannot invoke this service without a custom database back-end");
     }
   }
 
@@ -2392,15 +2477,25 @@
 
       case _OrthancPluginService_GetConfigurationPath:
       {
-        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = 
-          CopyString(Configuration::GetConfigurationAbsolutePath());
+        std::string s;
+
+        {
+          OrthancConfiguration::ReaderLock lock;
+          s = lock.GetConfiguration().GetConfigurationAbsolutePath();
+        }
+
+        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
         return true;
       }
 
       case _OrthancPluginService_GetConfiguration:
       {
         std::string s;
-        Configuration::FormatConfiguration(s);
+
+        {
+          OrthancConfiguration::ReaderLock lock;
+          lock.GetConfiguration().Format(s);
+        }
 
         *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
         return true;
@@ -2490,6 +2585,10 @@
         SetHttpHeader(parameters);
         return true;
 
+      case _OrthancPluginService_SetHttpErrorDetails:
+        SetHttpErrorDetails(parameters);
+        return true;
+
       case _OrthancPluginService_LookupPatient:
       case _OrthancPluginService_LookupStudy:
       case _OrthancPluginService_LookupStudyWithAccessionNumber:
@@ -2553,8 +2652,8 @@
       {
         const _OrthancPluginStartMultipartAnswer& p =
           *reinterpret_cast<const _OrthancPluginStartMultipartAnswer*>(parameters);
-        HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output);
-        output->StartMultipart(p.subType, p.contentType);
+        HttpOutput& output = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+        output.StartMultipart(p.subType, p.contentType);
         return true;
       }
 
@@ -2665,7 +2764,12 @@
       {
         const _OrthancPluginReturnSingleValue& p =
           *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters);
-        *(p.resultUint32) = Configuration::GetFontRegistry().GetSize();
+
+        {
+          OrthancConfiguration::ReaderLock lock;
+          *(p.resultUint32) = lock.GetConfiguration().GetFontRegistry().GetSize();
+        }
+
         return true;
       }
 
@@ -3171,8 +3275,8 @@
 
         if (pimpl_->database_.get() == NULL)
         {
-          LOG(ERROR) << "The service ReconstructMainDicomTags can only be invoked by custom database plugins";
-          throw OrthancException(ErrorCode_DatabasePlugin);
+          throw OrthancException(ErrorCode_DatabasePlugin,
+                                 "The service ReconstructMainDicomTags can only be invoked by custom database plugins");
         }
 
         IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
--- a/Plugins/Engine/OrthancPlugins.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Engine/OrthancPlugins.h	Thu Dec 06 15:58:08 2018 +0100
@@ -148,6 +148,8 @@
 
     void SetHttpHeader(const void* parameters);
 
+    void SetHttpErrorDetails(const void* parameters);
+
     void BufferCompression(const void* parameters);
 
     void UncompressImage(const void* parameters);
--- a/Plugins/Engine/PluginsJob.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Engine/PluginsJob.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -148,8 +148,8 @@
       if (!reader.parse(content, value) ||
           value.type() != Json::objectValue)
       {
-        LOG(ERROR) << "A job plugin must provide a JSON object as its public content";
-        throw OrthancException(ErrorCode_Plugin);
+        throw OrthancException(ErrorCode_Plugin,
+                               "A job plugin must provide a JSON object as its public content");
       }
     }
   }
@@ -169,8 +169,8 @@
       if (!reader.parse(serialized, value) ||
           value.type() != Json::objectValue)
       {
-        LOG(ERROR) << "A job plugin must provide a JSON object as its serialized content";
-        throw OrthancException(ErrorCode_Plugin);
+        throw OrthancException(ErrorCode_Plugin,
+                               "A job plugin must provide a JSON object as its serialized content");
       }
 
 
@@ -178,8 +178,8 @@
       
       if (value.isMember(KEY_TYPE))
       {
-        LOG(ERROR) << "The \"Type\" field is for reserved use for serialized job";
-        throw OrthancException(ErrorCode_Plugin);
+        throw OrthancException(ErrorCode_Plugin,
+                               "The \"Type\" field is for reserved use for serialized job");
       }
 
       value[KEY_TYPE] = type_;
--- a/Plugins/Engine/PluginsJob.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Engine/PluginsJob.h	Thu Dec 06 15:58:08 2018 +0100
@@ -71,6 +71,14 @@
     virtual void GetPublicContent(Json::Value& value);
 
     virtual bool Serialize(Json::Value& value);
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key)
+    {
+      // TODO
+      return false;
+    }
   };
 }
 
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Dec 06 15:58:08 2018 +0100
@@ -119,7 +119,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     4
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  2
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  3
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -451,6 +451,7 @@
     _OrthancPluginService_SendHttpStatus = 2010,
     _OrthancPluginService_CompressAndAnswerImage = 2011,
     _OrthancPluginService_SendMultipartItem2 = 2012,
+    _OrthancPluginService_SetHttpErrorDetails = 2013,
 
     /* Access to the Orthanc database and API */
     _OrthancPluginService_GetDicomForInstance = 3000,
@@ -1679,11 +1680,16 @@
    * OrthancPluginInitialize() public function.
    *
    * Each REST callback is guaranteed to run in mutual exclusion.
-   * 
+   *
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param pathRegularExpression Regular expression for the URI. May contain groups.
+   * @param pathRegularExpression Regular expression for the URI. May contain groups. 
    * @param callback The callback function to handle the REST call.
    * @see OrthancPluginRegisterRestCallbackNoLock()
+   *
+   * @note
+   * The regular expression is case sensitive and must follow the
+   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
+   *
    * @ingroup Callbacks
    **/
   ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallback(
@@ -1711,13 +1717,19 @@
    * will NOT be invoked in mutual exclusion. This can be useful for
    * high-performance plugins that must handle concurrent requests
    * (Orthanc uses a pool of threads, one thread being assigned to
-   * each incoming HTTP request). Of course, it is up to the plugin to
-   * implement the required locking mechanisms.
+   * each incoming HTTP request). Of course, if using this function,
+   * it is up to the plugin to implement the required locking
+   * mechanisms.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param pathRegularExpression Regular expression for the URI. May contain groups.
    * @param callback The callback function to handle the REST call.
    * @see OrthancPluginRegisterRestCallback()
+   *
+   * @note
+   * The regular expression is case sensitive and must follow the
+   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
+   *
    * @ingroup Callbacks
    **/
   ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallbackNoLock(
@@ -6416,6 +6428,46 @@
   }
 
 
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              details;
+    uint8_t                  log;
+  } _OrthancPluginSetHttpErrorDetails;
+
+  /**
+   * @brief Provide a detailed description for an HTTP error.
+   *
+   * This function sets the detailed description associated with an
+   * HTTP error. This description will be displayed in the "Details"
+   * field of the JSON body of the HTTP answer. It is only taken into
+   * consideration if the REST callback returns an error code that is
+   * different from "OrthancPluginErrorCode_Success", and if the
+   * "HttpDescribeErrors" configuration option of Orthanc is set to
+   * "true".
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param details The details of the error message.
+   * @param log Whether to also write the detailed error to the Orthanc logs.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpErrorDetails(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              details,
+    uint8_t                  log)
+  {
+    _OrthancPluginSetHttpErrorDetails params;
+    params.output = output;
+    params.details = details;
+    params.log = log;
+    context->InvokeService(context, _OrthancPluginService_SetHttpErrorDetails, &params);
+  }
+
+
+
 #ifdef  __cplusplus
 }
 #endif
--- a/Plugins/Samples/Basic/Plugin.c	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Samples/Basic/Plugin.c	Thu Dec 06 15:58:08 2018 +0100
@@ -36,6 +36,14 @@
   char buffer[1024];
   uint32_t i;
 
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    // 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");
+    return OrthancPluginErrorCode_ParameterOutOfRange;
+  }
+  
   sprintf(buffer, "Callback on URL [%s] with body [%s]\n", url, request->body);
   OrthancPluginLogWarning(context, buffer);
 
@@ -184,7 +192,7 @@
   if (request->method != OrthancPluginHttpMethod_Get)
   {
     OrthancPluginSendMethodNotAllowed(context, output, "GET");
-    return 0;
+    return OrthancPluginErrorCode_Success;
   }
 
   isBuiltIn = strcmp("plugins", request->groups[0]);
@@ -301,22 +309,51 @@
                                                             OrthancPluginResourceType resourceType,
                                                             const char* resourceId)
 {
+  OrthancPluginMemoryBuffer tmp;
+  memset(&tmp, 0, sizeof(tmp));
+
   char info[1024];
-  OrthancPluginMemoryBuffer tmp;
-
-  sprintf(info, "Change %d on resource %s of type %d", changeType, resourceId, resourceType);
+  sprintf(info, "Change %d on resource %s of type %d", changeType,
+          (resourceId == NULL ? "<none>" : resourceId), resourceType);
   OrthancPluginLogWarning(context, info);
 
-  if (changeType == OrthancPluginChangeType_NewInstance)
+  switch (changeType)
   {
-    sprintf(info, "/instances/%s/metadata/AnonymizedFrom", resourceId);
-    if (OrthancPluginRestApiGet(context, &tmp, info) == 0)
+    case OrthancPluginChangeType_NewInstance:
+    {
+      sprintf(info, "/instances/%s/metadata/AnonymizedFrom", resourceId);
+      if (OrthancPluginRestApiGet(context, &tmp, info) == 0)
+      {
+        sprintf(info, "  Instance %s comes from the anonymization of instance", resourceId);
+        strncat(info, (const char*) tmp.data, tmp.size);
+        OrthancPluginLogWarning(context, info);
+        OrthancPluginFreeMemoryBuffer(context, &tmp);
+      }
+
+      break;
+    }
+
+    case OrthancPluginChangeType_OrthancStarted:
     {
-      sprintf(info, "  Instance %s comes from the anonymization of instance", resourceId);
-      strncat(info, (const char*) tmp.data, tmp.size);
-      OrthancPluginLogWarning(context, info);
+      /* Make REST requests to the built-in Orthanc API */
+      OrthancPluginRestApiGet(context, &tmp, "/changes");
+      OrthancPluginFreeMemoryBuffer(context, &tmp);
+      OrthancPluginRestApiGet(context, &tmp, "/changes?limit=1");
       OrthancPluginFreeMemoryBuffer(context, &tmp);
+
+      /* Play with PUT by defining a new target modality. */
+      sprintf(info, "[ \"STORESCP\", \"localhost\", 2000 ]");
+      OrthancPluginRestApiPut(context, &tmp, "/modalities/demo", info, strlen(info));
+
+      break;
     }
+
+    case OrthancPluginChangeType_OrthancStopped:
+      OrthancPluginLogWarning(context, "Orthanc has stopped");
+      break;
+
+    default:
+      break;
   }
 
   return OrthancPluginErrorCode_Success;
@@ -357,7 +394,6 @@
 
 ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
 {
-  OrthancPluginMemoryBuffer tmp;
   char info[1024], *s;
   int counter, i;
   OrthancPluginDictionaryEntry entry;
@@ -425,19 +461,8 @@
   OrthancPluginSetDescription(context, "This is the description of the sample plugin that can be seen in Orthanc Explorer.");
   OrthancPluginExtendOrthancExplorer(context, "alert('Hello Orthanc! From sample plugin with love.');");
 
-  /* Make REST requests to the built-in Orthanc API */
-  memset(&tmp, 0, sizeof(tmp));
-  OrthancPluginRestApiGet(context, &tmp, "/changes");
-  OrthancPluginFreeMemoryBuffer(context, &tmp);
-  OrthancPluginRestApiGet(context, &tmp, "/changes?limit=1");
-  OrthancPluginFreeMemoryBuffer(context, &tmp);
+  customError = OrthancPluginRegisterErrorCode(context, 4, 402, "Hello world");
   
-  /* Play with PUT by defining a new target modality. */
-  sprintf(info, "[ \"STORESCP\", \"localhost\", 2000 ]");
-  OrthancPluginRestApiPut(context, &tmp, "/modalities/demo", info, strlen(info));
-
-  customError = OrthancPluginRegisterErrorCode(context, 4, 402, "Hello world");
-
   OrthancPluginRegisterDictionaryTag(context, 0x0014, 0x1020, OrthancPluginValueRepresentation_DA,
                                      "ValidationExpiryDate", 1, 1);
 
--- a/Plugins/Samples/Common/FullOrthancDataset.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Samples/Common/FullOrthancDataset.h	Thu Dec 06 15:58:08 2018 +0100
@@ -65,5 +65,10 @@
 
     virtual bool GetSequenceSize(size_t& size,
                                  const DicomPath& path) const;
+
+    FullOrthancDataset* Clone() const
+    {
+      return new FullOrthancDataset(this->root_);
+    }
   };
 }
--- a/Plugins/Samples/Common/OrthancPluginConnection.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Samples/Common/OrthancPluginConnection.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -40,7 +40,7 @@
   void OrthancPluginConnection::RestApiGet(std::string& result,
                                            const std::string& uri) 
   {
-    OrthancPlugins::MemoryBuffer buffer(context_);
+    OrthancPlugins::MemoryBuffer buffer;
 
     if (buffer.RestApiGet(uri, false))
     {
@@ -57,7 +57,7 @@
                                             const std::string& uri,
                                             const std::string& body)
   {
-    OrthancPlugins::MemoryBuffer buffer(context_);
+    OrthancPlugins::MemoryBuffer buffer;
 
     if (buffer.RestApiPost(uri, body.c_str(), body.size(), false))
     {
@@ -74,7 +74,7 @@
                                            const std::string& uri,
                                            const std::string& body)
   {
-    OrthancPlugins::MemoryBuffer buffer(context_);
+    OrthancPlugins::MemoryBuffer buffer;
 
     if (buffer.RestApiPut(uri, body.c_str(), body.size(), false))
     {
@@ -89,9 +89,9 @@
 
   void OrthancPluginConnection::RestApiDelete(const std::string& uri)
   {
-    OrthancPlugins::MemoryBuffer buffer(context_);
+    OrthancPlugins::MemoryBuffer buffer;
 
-    if (!::OrthancPlugins::RestApiDelete(context_, uri, false))
+    if (!::OrthancPlugins::RestApiDelete(uri, false))
     {
       ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
     }
--- a/Plugins/Samples/Common/OrthancPluginConnection.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Samples/Common/OrthancPluginConnection.h	Thu Dec 06 15:58:08 2018 +0100
@@ -42,15 +42,7 @@
   // This class is thread-safe
   class OrthancPluginConnection : public IOrthancConnection
   {
-  private:
-    OrthancPluginContext*   context_;
-
   public:
-    OrthancPluginConnection(OrthancPluginContext* context) :
-    context_(context)
-    {
-    }
-
     virtual void RestApiGet(std::string& result,
                             const std::string& uri);
 
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -39,6 +39,45 @@
 
 namespace OrthancPlugins
 {
+  static OrthancPluginContext* globalContext_ = NULL;
+
+  
+  void SetGlobalContext(OrthancPluginContext* context)
+  {
+    if (context == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer);
+    }        
+    else if (globalContext_ == NULL)
+    {
+      globalContext_ = context;
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);      
+    }
+  }
+
+  
+  bool HasGlobalContext()
+  {
+    return globalContext_ != NULL;
+  }
+  
+
+  OrthancPluginContext* GetGlobalContext()
+  {
+    if (globalContext_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);      
+    }
+    else
+    {
+      return globalContext_;
+    }
+  }
+
+
   void MemoryBuffer::Check(OrthancPluginErrorCode code)
   {
     if (code != OrthancPluginErrorCode_Success)
@@ -76,8 +115,7 @@
   }
 
 
-  MemoryBuffer::MemoryBuffer(OrthancPluginContext* context) :
-    context_(context)
+  MemoryBuffer::MemoryBuffer()
   {
     buffer_.data = NULL;
     buffer_.size = 0;
@@ -88,7 +126,7 @@
   {
     if (buffer_.data != NULL)
     {
-      OrthancPluginFreeMemoryBuffer(context_, &buffer_);
+      OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &buffer_);
       buffer_.data = NULL;
       buffer_.size = 0;
     }
@@ -144,7 +182,7 @@
     Json::Reader reader;
     if (!reader.parse(tmp, tmp + buffer_.size, target))
     {
-      OrthancPluginLogError(context_, "Cannot convert some memory buffer to JSON");
+      LogError("Cannot convert some memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
   }
@@ -157,11 +195,11 @@
 
     if (applyPlugins)
     {
-      return CheckHttp(OrthancPluginRestApiGetAfterPlugins(context_, &buffer_, uri.c_str()));
+      return CheckHttp(OrthancPluginRestApiGetAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str()));
     }
     else
     {
-      return CheckHttp(OrthancPluginRestApiGet(context_, &buffer_, uri.c_str()));
+      return CheckHttp(OrthancPluginRestApiGet(GetGlobalContext(), &buffer_, uri.c_str()));
     }
   }
 
@@ -175,11 +213,11 @@
 
     if (applyPlugins)
     {
-      return CheckHttp(OrthancPluginRestApiPostAfterPlugins(context_, &buffer_, uri.c_str(), body, bodySize));
+      return CheckHttp(OrthancPluginRestApiPostAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str(), body, bodySize));
     }
     else
     {
-      return CheckHttp(OrthancPluginRestApiPost(context_, &buffer_, uri.c_str(), body, bodySize));
+      return CheckHttp(OrthancPluginRestApiPost(GetGlobalContext(), &buffer_, uri.c_str(), body, bodySize));
     }
   }
 
@@ -193,11 +231,11 @@
 
     if (applyPlugins)
     {
-      return CheckHttp(OrthancPluginRestApiPutAfterPlugins(context_, &buffer_, uri.c_str(), body, bodySize));
+      return CheckHttp(OrthancPluginRestApiPutAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str(), body, bodySize));
     }
     else
     {
-      return CheckHttp(OrthancPluginRestApiPut(context_, &buffer_, uri.c_str(), body, bodySize));
+      return CheckHttp(OrthancPluginRestApiPut(GetGlobalContext(), &buffer_, uri.c_str(), body, bodySize));
     }
   }
 
@@ -228,7 +266,7 @@
     Json::FastWriter writer;
     std::string s = writer.write(tags);
     
-    Check(OrthancPluginCreateDicom(context_, &buffer_, s.c_str(), NULL, flags));
+    Check(OrthancPluginCreateDicom(GetGlobalContext(), &buffer_, s.c_str(), NULL, flags));
   }
 
   void MemoryBuffer::CreateDicom(const Json::Value& tags,
@@ -240,21 +278,21 @@
     Json::FastWriter writer;
     std::string s = writer.write(tags);
 
-    Check(OrthancPluginCreateDicom(context_, &buffer_, s.c_str(), pixelData.GetObject(), flags));
+    Check(OrthancPluginCreateDicom(GetGlobalContext(), &buffer_, s.c_str(), pixelData.GetObject(), flags));
   }
 
 
   void MemoryBuffer::ReadFile(const std::string& path)
   {
     Clear();
-    Check(OrthancPluginReadFile(context_, &buffer_, path.c_str()));
+    Check(OrthancPluginReadFile(GetGlobalContext(), &buffer_, path.c_str()));
   }
 
 
   void MemoryBuffer::GetDicomQuery(const OrthancPluginWorklistQuery* query)
   {
     Clear();
-    Check(OrthancPluginWorklistGetDicomQuery(context_, &buffer_, query));
+    Check(OrthancPluginWorklistGetDicomQuery(GetGlobalContext(), &buffer_, query));
   }
 
 
@@ -276,7 +314,7 @@
   {
     if (str_ != NULL)
     {
-      OrthancPluginFreeString(context_, str_);
+      OrthancPluginFreeString(GetGlobalContext(), str_);
       str_ = NULL;
     }
   }
@@ -299,14 +337,14 @@
   {
     if (str_ == NULL)
     {
-      OrthancPluginLogError(context_, "Cannot convert an empty memory buffer to JSON");
+      LogError("Cannot convert an empty memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
     Json::Reader reader;
     if (!reader.parse(str_, target))
     {
-      OrthancPluginLogError(context_, "Cannot convert some memory buffer to JSON");
+      LogError("Cannot convert some memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
   }
@@ -317,8 +355,9 @@
                                  OrthancPluginDicomToJsonFlags flags,
                                  uint32_t maxStringLength)
   {
-    OrthancString str(context_);
-    str.Assign(OrthancPluginDicomBufferToJson(context_, GetData(), GetSize(), format, flags, maxStringLength));
+    OrthancString str;
+    str.Assign(OrthancPluginDicomBufferToJson
+               (GetGlobalContext(), GetData(), GetSize(), format, flags, maxStringLength));
     str.ToJson(target);
   }
 
@@ -328,7 +367,7 @@
                              const std::string& password)
   {
     Clear();
-    return CheckHttp(OrthancPluginHttpGet(context_, &buffer_, url.c_str(),
+    return CheckHttp(OrthancPluginHttpGet(GetGlobalContext(), &buffer_, url.c_str(),
                                           username.empty() ? NULL : username.c_str(),
                                           password.empty() ? NULL : password.c_str()));
   }
@@ -340,7 +379,7 @@
                               const std::string& password)
   {
     Clear();
-    return CheckHttp(OrthancPluginHttpPost(context_, &buffer_, url.c_str(),
+    return CheckHttp(OrthancPluginHttpPost(GetGlobalContext(), &buffer_, url.c_str(),
                                            body.c_str(), body.size(),
                                            username.empty() ? NULL : username.c_str(),
                                            password.empty() ? NULL : password.c_str()));
@@ -353,7 +392,7 @@
                              const std::string& password)
   {
     Clear();
-    return CheckHttp(OrthancPluginHttpPut(context_, &buffer_, url.c_str(),
+    return CheckHttp(OrthancPluginHttpPut(GetGlobalContext(), &buffer_, url.c_str(),
                                           body.empty() ? NULL : body.c_str(),
                                           body.size(),
                                           username.empty() ? NULL : username.c_str(),
@@ -364,17 +403,16 @@
   void MemoryBuffer::GetDicomInstance(const std::string& instanceId)
   {
     Clear();
-    Check(OrthancPluginGetDicomForInstance(context_, &buffer_, instanceId.c_str()));
+    Check(OrthancPluginGetDicomForInstance(GetGlobalContext(), &buffer_, instanceId.c_str()));
   }
 
   
-  bool HttpDelete(OrthancPluginContext* context_,
-                  const std::string& url,
+  bool HttpDelete(const std::string& url,
                   const std::string& username,
                   const std::string& password)
   {
     OrthancPluginErrorCode error = OrthancPluginHttpDelete
-        (context_, url.c_str(),
+        (GetGlobalContext(), url.c_str(),
          username.empty() ? NULL : username.c_str(),
          password.empty() ? NULL : password.c_str());
 
@@ -394,15 +432,41 @@
   }
   
 
-  OrthancConfiguration::OrthancConfiguration(OrthancPluginContext* context) :
-    context_(context)
+  void LogError(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogError(GetGlobalContext(), message.c_str());
+    }
+  }
+
+  
+  void LogWarning(const std::string& message)
   {
-    OrthancString str(context);
-    str.Assign(OrthancPluginGetConfiguration(context));
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogWarning(GetGlobalContext(), message.c_str());
+    }
+  }
+
+  
+  void LogInfo(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogInfo(GetGlobalContext(), message.c_str());
+    }
+  }
+
+
+  OrthancConfiguration::OrthancConfiguration()
+  {
+    OrthancString str;
+    str.Assign(OrthancPluginGetConfiguration(GetGlobalContext()));
 
     if (str.GetContent() == NULL)
     {
-      OrthancPluginLogError(context, "Cannot access the Orthanc configuration");
+      LogError("Cannot access the Orthanc configuration");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -410,25 +474,12 @@
 
     if (configuration_.type() != Json::objectValue)
     {
-      OrthancPluginLogError(context, "Unable to read the Orthanc configuration");
+      LogError("Unable to read the Orthanc configuration");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
   }
 
 
-  OrthancPluginContext* OrthancConfiguration::GetContext() const
-  {
-    if (context_ == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
-    }
-    else
-    {
-      return context_;
-    }
-  }
-
-
   std::string OrthancConfiguration::GetPath(const std::string& key) const
   {
     if (path_.empty())
@@ -456,7 +507,6 @@
   {
     assert(configuration_.type() == Json::objectValue);
 
-    target.context_ = context_;
     target.path_ = GetPath(key);
 
     if (!configuration_.isMember(key))
@@ -467,11 +517,8 @@
     {
       if (configuration_[key].type() != Json::objectValue)
       {
-        if (context_ != NULL)
-        {
-          std::string s = "The configuration section \"" + target.path_ + "\" is not an associative array as expected";
-          OrthancPluginLogError(context_, s.c_str());
-        }
+        LogError("The configuration section \"" + target.path_ +
+                 "\" is not an associative array as expected");
 
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
       }
@@ -493,11 +540,8 @@
 
     if (configuration_[key].type() != Json::stringValue)
     {
-      if (context_ != NULL)
-      {
-        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a string as expected";
-        OrthancPluginLogError(context_, s.c_str());
-      }
+      LogError("The configuration option \"" + GetPath(key) +
+               "\" is not a string as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -528,11 +572,8 @@
       return true;
 
     default:
-      if (context_ != NULL)
-      {
-        std::string s = "The configuration option \"" + GetPath(key) + "\" is not an integer as expected";
-        OrthancPluginLogError(context_, s.c_str());
-      }
+      LogError("The configuration option \"" + GetPath(key) +
+               "\" is not an integer as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -550,11 +591,8 @@
 
     if (tmp < 0)
     {
-      if (context_ != NULL)
-      {
-        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a positive integer as expected";
-        OrthancPluginLogError(context_, s.c_str());
-      }
+      LogError("The configuration option \"" + GetPath(key) +
+               "\" is not a positive integer as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -578,11 +616,8 @@
 
     if (configuration_[key].type() != Json::booleanValue)
     {
-      if (context_ != NULL)
-      {
-        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a Boolean as expected";
-        OrthancPluginLogError(context_, s.c_str());
-      }
+      LogError("The configuration option \"" + GetPath(key) +
+               "\" is not a Boolean as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -617,11 +652,8 @@
       return true;
 
     default:
-      if (context_ != NULL)
-      {
-        std::string s = "The configuration option \"" + GetPath(key) + "\" is not an integer as expected";
-        OrthancPluginLogError(context_, s.c_str());
-      }
+      LogError("The configuration option \"" + GetPath(key) +
+               "\" is not an integer as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -680,12 +712,8 @@
       break;
     }
 
-    if (context_ != NULL)
-    {
-      std::string s = ("The configuration option \"" + GetPath(key) +
-                       "\" is not a list of strings as expected");
-      OrthancPluginLogError(context_, s.c_str());
-    }
+    LogError("The configuration option \"" + GetPath(key) +
+             "\" is not a list of strings as expected");
 
     ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
   }
@@ -805,11 +833,8 @@
 
     if (configuration_[key].type() != Json::objectValue)
     {
-      if (context_ != NULL)
-      {
-        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a string as expected";
-        OrthancPluginLogError(context_, s.c_str());
-      }
+      LogError("The configuration option \"" + GetPath(key) +
+               "\" is not a string as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -826,11 +851,8 @@
       }
       else
       {
-        if (context_ != NULL)
-        {
-          std::string s = "The configuration option \"" + GetPath(key) + "\" is not a dictionary mapping strings to strings";
-          OrthancPluginLogError(context_, s.c_str());
-        }
+        LogError("The configuration option \"" + GetPath(key) +
+                 "\" is not a dictionary mapping strings to strings");
 
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
       }
@@ -842,7 +864,7 @@
   {
     if (image_ != NULL)
     {
-      OrthancPluginFreeImage(context_, image_);
+      OrthancPluginFreeImage(GetGlobalContext(), image_);
       image_ = NULL;
     }
   }
@@ -852,66 +874,51 @@
   {
     if (image_ == NULL)
     {
-      OrthancPluginLogError(context_, "Trying to access a NULL image");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
-    }
-  }
-
-
-  OrthancImage::OrthancImage(OrthancPluginContext*  context) :
-    context_(context),
-    image_(NULL)
-  {
-    if (context == NULL)
-    {
+      LogError("Trying to access a NULL image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
 
 
-  OrthancImage::OrthancImage(OrthancPluginContext*  context,
-                             OrthancPluginImage*    image) :
-    context_(context),
+  OrthancImage::OrthancImage() :
+    image_(NULL)
+  {
+  }
+
+
+  OrthancImage::OrthancImage(OrthancPluginImage*  image) :
     image_(image)
   {
-    if (context == NULL)
+  }
+  
+
+  OrthancImage::OrthancImage(OrthancPluginPixelFormat  format,
+                             uint32_t                  width,
+                             uint32_t                  height)
+  {
+    image_ = OrthancPluginCreateImage(GetGlobalContext(), format, width, height);
+
+    if (image_ == NULL)
     {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+      LogError("Cannot create an image");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
   }
   
 
-  OrthancImage::OrthancImage(OrthancPluginContext*     context,
-                             OrthancPluginPixelFormat  format,
-                             uint32_t                  width,
-                             uint32_t                  height) :
-    context_(context)
-  {
-    if (context == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
-    }
-    else
-    {
-      image_ = OrthancPluginCreateImage(context, format, width, height);
-    }
-  }
-
-  OrthancImage::OrthancImage(OrthancPluginContext*     context,
-                             OrthancPluginPixelFormat  format,
+  OrthancImage::OrthancImage(OrthancPluginPixelFormat  format,
                              uint32_t                  width,
                              uint32_t                  height,
                              uint32_t                  pitch,
-                             void*                     buffer) :
-    context_(context)
+                             void*                     buffer)
   {
-    if (context == NULL)
+    image_ = OrthancPluginCreateImageAccessor
+      (GetGlobalContext(), format, width, height, pitch, buffer);
+
+    if (image_ == NULL)
     {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
-    }
-    else
-    {
-      image_ = OrthancPluginCreateImageAccessor(context, format, width, height, pitch, buffer);
+      LogError("Cannot create an image accessor");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
   }
 
@@ -919,10 +926,12 @@
                                         size_t size)
   {
     Clear();
-    image_ = OrthancPluginUncompressImage(context_, data, size, OrthancPluginImageFormat_Png);
+    
+    image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Png);
+
     if (image_ == NULL)
     {
-      OrthancPluginLogError(context_, "Cannot uncompress a PNG image");
+      LogError("Cannot uncompress a PNG image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -932,10 +941,10 @@
                                          size_t size)
   {
     Clear();
-    image_ = OrthancPluginUncompressImage(context_, data, size, OrthancPluginImageFormat_Jpeg);
+    image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Jpeg);
     if (image_ == NULL)
     {
-      OrthancPluginLogError(context_, "Cannot uncompress a JPEG image");
+      LogError("Cannot uncompress a JPEG image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -946,10 +955,10 @@
                                       unsigned int frame)
   {
     Clear();
-    image_ = OrthancPluginDecodeDicomImage(context_, data, size, frame);
+    image_ = OrthancPluginDecodeDicomImage(GetGlobalContext(), data, size, frame);
     if (image_ == NULL)
     {
-      OrthancPluginLogError(context_, "Cannot uncompress a DICOM image");
+      LogError("Cannot uncompress a DICOM image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -958,35 +967,35 @@
   OrthancPluginPixelFormat OrthancImage::GetPixelFormat()
   {
     CheckImageAvailable();
-    return OrthancPluginGetImagePixelFormat(context_, image_);
+    return OrthancPluginGetImagePixelFormat(GetGlobalContext(), image_);
   }
 
 
   unsigned int OrthancImage::GetWidth()
   {
     CheckImageAvailable();
-    return OrthancPluginGetImageWidth(context_, image_);
+    return OrthancPluginGetImageWidth(GetGlobalContext(), image_);
   }
 
 
   unsigned int OrthancImage::GetHeight()
   {
     CheckImageAvailable();
-    return OrthancPluginGetImageHeight(context_, image_);
+    return OrthancPluginGetImageHeight(GetGlobalContext(), image_);
   }
 
 
   unsigned int OrthancImage::GetPitch()
   {
     CheckImageAvailable();
-    return OrthancPluginGetImagePitch(context_, image_);
+    return OrthancPluginGetImagePitch(GetGlobalContext(), image_);
   }
 
 
   const void* OrthancImage::GetBuffer()
   {
     CheckImageAvailable();
-    return OrthancPluginGetImageBuffer(context_, image_);
+    return OrthancPluginGetImageBuffer(GetGlobalContext(), image_);
   }
 
 
@@ -995,7 +1004,7 @@
     CheckImageAvailable();
     
     OrthancPluginMemoryBuffer tmp;
-    OrthancPluginCompressPngImage(context_, &tmp, GetPixelFormat(),
+    OrthancPluginCompressPngImage(GetGlobalContext(), &tmp, GetPixelFormat(),
                                   GetWidth(), GetHeight(), GetPitch(), GetBuffer());
 
     target.Assign(tmp);
@@ -1008,7 +1017,7 @@
     CheckImageAvailable();
     
     OrthancPluginMemoryBuffer tmp;
-    OrthancPluginCompressJpegImage(context_, &tmp, GetPixelFormat(),
+    OrthancPluginCompressJpegImage(GetGlobalContext(), &tmp, GetPixelFormat(),
                                    GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality);
     
     target.Assign(tmp);
@@ -1018,7 +1027,7 @@
   void OrthancImage::AnswerPngImage(OrthancPluginRestOutput* output)
   {
     CheckImageAvailable();
-    OrthancPluginCompressAndAnswerPngImage(context_, output, GetPixelFormat(),
+    OrthancPluginCompressAndAnswerPngImage(GetGlobalContext(), output, GetPixelFormat(),
                                            GetWidth(), GetHeight(), GetPitch(), GetBuffer());
   }
 
@@ -1027,16 +1036,14 @@
                                      uint8_t quality)
   {
     CheckImageAvailable();
-    OrthancPluginCompressAndAnswerJpegImage(context_, output, GetPixelFormat(),
+    OrthancPluginCompressAndAnswerJpegImage(GetGlobalContext(), output, GetPixelFormat(),
                                             GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality);
   }
 
 
 
 #if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1
-  FindMatcher::FindMatcher(OrthancPluginContext*              context,
-                           const OrthancPluginWorklistQuery*  worklist) :
-    context_(context),
+  FindMatcher::FindMatcher(const OrthancPluginWorklistQuery* worklist) :
     matcher_(NULL),
     worklist_(worklist)
   {
@@ -1047,14 +1054,12 @@
   }
 
 
-  void FindMatcher::SetupDicom(OrthancPluginContext*  context,
-                               const void*            query,
-                               uint32_t               size)
+  void FindMatcher::SetupDicom(const void*  query,
+                               uint32_t     size)
   {
-    context_ = context;
     worklist_ = NULL;
 
-    matcher_ = OrthancPluginCreateFindMatcher(context_, query, size);
+    matcher_ = OrthancPluginCreateFindMatcher(GetGlobalContext(), query, size);
     if (matcher_ == NULL)
     {
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
@@ -1068,7 +1073,7 @@
 
     if (matcher_ != NULL)
     {
-      OrthancPluginFreeFindMatcher(context_, matcher_);
+      OrthancPluginFreeFindMatcher(GetGlobalContext(), matcher_);
     }
   }
 
@@ -1081,11 +1086,11 @@
 
     if (matcher_ != NULL)
     {
-      result = OrthancPluginFindMatcherIsMatch(context_, matcher_, dicom, size);
+      result = OrthancPluginFindMatcherIsMatch(GetGlobalContext(), matcher_, dicom, size);
     }
     else if (worklist_ != NULL)
     {
-      result = OrthancPluginWorklistIsMatch(context_, worklist_, dicom, size);
+      result = OrthancPluginWorklistIsMatch(GetGlobalContext(), worklist_, dicom, size);
     }
     else
     {
@@ -1108,100 +1113,142 @@
 
 #endif /* HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 */
 
+  void AnswerJson(const Json::Value& value,
+                  OrthancPluginRestOutput* output
+                  )
+  {
+    Json::StyledWriter writer;
+    std::string bodyString = writer.write(value);
 
-  bool RestApiGet(Json::Value& result,
-                  OrthancPluginContext* context,
-                  const std::string& uri,
-                  bool applyPlugins)
+    OrthancPluginAnswerBuffer(GetGlobalContext(), output, bodyString.c_str(), bodyString.size(), "application/json");
+  }
+
+  void AnswerHttpError(uint16_t httpError, OrthancPluginRestOutput *output)
   {
-    MemoryBuffer answer(context);
+    OrthancPluginSendHttpStatusCode(GetGlobalContext(), output, httpError);
+  }
+
+  void AnswerMethodNotAllowed(OrthancPluginRestOutput *output, const char* allowedMethods)
+  {
+    OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowedMethods);
+  }
+
+  bool RestApiGetString(std::string& result,
+                        const std::string& uri,
+                        bool applyPlugins)
+  {
+    MemoryBuffer answer;
     if (!answer.RestApiGet(uri, applyPlugins))
     {
       return false;
     }
     else
     {
-      answer.ToJson(result);
+      answer.ToString(result);
       return true;
     }
   }
 
 
-  bool RestApiPost(Json::Value& result,
-                   OrthancPluginContext* context,
-                   const std::string& uri,
-                   const char* body,
-                   size_t bodySize,
-                   bool applyPlugins)
+  bool RestApiGet(Json::Value& result,
+                  const std::string& uri,
+                  bool applyPlugins)
   {
-    MemoryBuffer answer(context);
-    if (!answer.RestApiPost(uri, body, bodySize, applyPlugins))
+    MemoryBuffer answer;
+    
+    if (!answer.RestApiGet(uri, applyPlugins))
     {
       return false;
     }
     else
     {
-      answer.ToJson(result);
+      if (!answer.IsEmpty())
+      {
+        answer.ToJson(result);
+      }
       return true;
     }
   }
 
 
   bool RestApiPost(Json::Value& result,
-                   OrthancPluginContext* context,
+                   const std::string& uri,
+                   const char* body,
+                   size_t bodySize,
+                   bool applyPlugins)
+  {
+    MemoryBuffer answer;
+    
+    if (!answer.RestApiPost(uri, body, bodySize, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      if (!answer.IsEmpty())
+      {
+        answer.ToJson(result);
+      }
+      return true;
+    }
+  }
+
+
+  bool RestApiPost(Json::Value& result,
                    const std::string& uri,
                    const Json::Value& body,
                    bool applyPlugins)
   {
     Json::FastWriter writer;
-    return RestApiPost(result, context, uri, writer.write(body), applyPlugins);
+    return RestApiPost(result, uri, writer.write(body), applyPlugins);
   }
 
 
   bool RestApiPut(Json::Value& result,
-                  OrthancPluginContext* context,
                   const std::string& uri,
                   const char* body,
                   size_t bodySize,
                   bool applyPlugins)
   {
-    MemoryBuffer answer(context);
+    MemoryBuffer answer;
+    
     if (!answer.RestApiPut(uri, body, bodySize, applyPlugins))
     {
       return false;
     }
     else
     {
-      answer.ToJson(result);
+      if (!answer.IsEmpty()) // i.e, on a PUT to metadata/..., orthand returns an empty response
+      {
+        answer.ToJson(result);
+      }
       return true;
     }
   }
 
 
   bool RestApiPut(Json::Value& result,
-                  OrthancPluginContext* context,
                   const std::string& uri,
                   const Json::Value& body,
                   bool applyPlugins)
   {
     Json::FastWriter writer;
-    return RestApiPut(result, context, uri, writer.write(body), applyPlugins);
+    return RestApiPut(result, uri, writer.write(body), applyPlugins);
   }
 
 
-  bool RestApiDelete(OrthancPluginContext* context,
-                     const std::string& uri,
+  bool RestApiDelete(const std::string& uri,
                      bool applyPlugins)
   {
     OrthancPluginErrorCode error;
 
     if (applyPlugins)
     {
-      error = OrthancPluginRestApiDeleteAfterPlugins(context, uri.c_str());
+      error = OrthancPluginRestApiDeleteAfterPlugins(GetGlobalContext(), uri.c_str());
     }
     else
     {
-      error = OrthancPluginRestApiDelete(context, uri.c_str());
+      error = OrthancPluginRestApiDelete(GetGlobalContext(), uri.c_str());
     }
 
     if (error == OrthancPluginErrorCode_Success)
@@ -1220,35 +1267,31 @@
   }
 
 
-  void ReportMinimalOrthancVersion(OrthancPluginContext* context,
-                                   unsigned int major,
+  void ReportMinimalOrthancVersion(unsigned int major,
                                    unsigned int minor,
                                    unsigned int revision)
   {
-    std::string s = ("Your version of the Orthanc core (" +
-                     std::string(context->orthancVersion) +
-                     ") is too old to run this plugin (version " +
-                     boost::lexical_cast<std::string>(major) + "." +
-                     boost::lexical_cast<std::string>(minor) + "." +
-                     boost::lexical_cast<std::string>(revision) +
-                     " is required)");
-    
-    OrthancPluginLogError(context, s.c_str());
+    LogError("Your version of the Orthanc core (" +
+             std::string(GetGlobalContext()->orthancVersion) +
+             ") is too old to run this plugin (version " +
+             boost::lexical_cast<std::string>(major) + "." +
+             boost::lexical_cast<std::string>(minor) + "." +
+             boost::lexical_cast<std::string>(revision) +
+             " is required)");
   }
 
 
-  bool CheckMinimalOrthancVersion(OrthancPluginContext* context,
-                                  unsigned int major,
+  bool CheckMinimalOrthancVersion(unsigned int major,
                                   unsigned int minor,
                                   unsigned int revision)
   {
-    if (context == NULL)
+    if (!HasGlobalContext())
     {
-      OrthancPluginLogError(context, "Bad Orthanc context in the plugin");
+      LogError("Bad Orthanc context in the plugin");
       return false;
     }
 
-    if (!strcmp(context->orthancVersion, "mainline"))
+    if (!strcmp(GetGlobalContext()->orthancVersion, "mainline"))
     {
       // Assume compatibility with the mainline
       return true;
@@ -1262,7 +1305,7 @@
     #else
         sscanf
     #endif
-        (context->orthancVersion, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 ||
+        (GetGlobalContext()->orthancVersion, "%4d.%4d.%4d", &aa, &bb, &cc) != 3 ||
         aa < 0 ||
         bb < 0 ||
         cc < 0)
@@ -1313,6 +1356,58 @@
     }
   }
 
+  const char* GetMimeType(const std::string& path)
+  {
+    size_t dot = path.find_last_of('.');
+
+    std::string extension = (dot == std::string::npos) ? "" : path.substr(dot);
+    std::transform(extension.begin(), extension.end(), extension.begin(), tolower);
+
+    if (extension == ".html")
+    {
+      return "text/html";
+    }
+    else if (extension == ".css")
+    {
+      return "text/css";
+    }
+    else if (extension == ".js")
+    {
+      return "application/javascript";
+    }
+    else if (extension == ".gif")
+    {
+      return "image/gif";
+    }
+    else if (extension == ".svg")
+    {
+      return "image/svg+xml";
+    }
+    else if (extension == ".json")
+    {
+      return "application/json";
+    }
+    else if (extension == ".xml")
+    {
+      return "application/xml";
+    }
+    else if (extension == ".wasm")
+    {
+      return "application/wasm";
+    }
+    else if (extension == ".png")
+    {
+      return "image/png";
+    }
+    else if (extension == ".jpg" || extension == ".jpeg")
+    {
+      return "image/jpeg";
+    }
+    else
+    {
+      return "application/octet-stream";
+    }
+  }
 
 
 
@@ -1326,38 +1421,31 @@
     }
     else
     {
-      std::string s = "Inexistent peer: " + name;
-      OrthancPluginLogError(context_, s.c_str());
+      LogError("Inexistent peer: " + name);
       ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
     }
   }
 
 
-  OrthancPeers::OrthancPeers(OrthancPluginContext* context) :
-    context_(context),
+  OrthancPeers::OrthancPeers() :
     peers_(NULL),
     timeout_(0)
   {
-    if (context_ == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
-    }
-
-    peers_ = OrthancPluginGetPeers(context_);
+    peers_ = OrthancPluginGetPeers(GetGlobalContext());
 
     if (peers_ == NULL)
     {
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
     }
 
-    uint32_t count = OrthancPluginGetPeersCount(context_, peers_);
+    uint32_t count = OrthancPluginGetPeersCount(GetGlobalContext(), peers_);
 
     for (uint32_t i = 0; i < count; i++)
     {
-      const char* name = OrthancPluginGetPeerName(context_, peers_, i);
+      const char* name = OrthancPluginGetPeerName(GetGlobalContext(), peers_, i);
       if (name == NULL)
       {
-        OrthancPluginFreePeers(context_, peers_);
+        OrthancPluginFreePeers(GetGlobalContext(), peers_);
         ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
       }
 
@@ -1370,7 +1458,7 @@
   {
     if (peers_ != NULL)
     {
-      OrthancPluginFreePeers(context_, peers_);
+      OrthancPluginFreePeers(GetGlobalContext(), peers_);
     }
   }
 
@@ -1400,7 +1488,7 @@
     }
     else
     {
-      const char* s = OrthancPluginGetPeerName(context_, peers_, static_cast<uint32_t>(index));
+      const char* s = OrthancPluginGetPeerName(GetGlobalContext(), peers_, static_cast<uint32_t>(index));
       if (s == NULL)
       {
         ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
@@ -1421,7 +1509,7 @@
     }
     else
     {
-      const char* s = OrthancPluginGetPeerUrl(context_, peers_, static_cast<uint32_t>(index));
+      const char* s = OrthancPluginGetPeerUrl(GetGlobalContext(), peers_, static_cast<uint32_t>(index));
       if (s == NULL)
       {
         ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
@@ -1450,7 +1538,7 @@
     }
     else
     {
-      const char* s = OrthancPluginGetPeerUserProperty(context_, peers_, static_cast<uint32_t>(index), key.c_str());
+      const char* s = OrthancPluginGetPeerUserProperty(GetGlobalContext(), peers_, static_cast<uint32_t>(index), key.c_str());
       if (s == NULL)
       {
         return false;
@@ -1484,7 +1572,7 @@
     OrthancPluginMemoryBuffer answer;
     uint16_t status;
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
-        (context_, &answer, NULL, &status, peers_,
+        (GetGlobalContext(), &answer, NULL, &status, peers_,
          static_cast<uint32_t>(index), OrthancPluginHttpMethod_Get, uri.c_str(),
          0, NULL, NULL, NULL, 0, timeout_);
 
@@ -1514,7 +1602,7 @@
                            size_t index,
                            const std::string& uri) const
   {
-    MemoryBuffer buffer(context_);
+    MemoryBuffer buffer;
 
     if (DoGet(buffer, index, uri))
     {
@@ -1532,7 +1620,7 @@
                            const std::string& name,
                            const std::string& uri) const
   {
-    MemoryBuffer buffer(context_);
+    MemoryBuffer buffer;
 
     if (DoGet(buffer, name, uri))
     {
@@ -1562,7 +1650,7 @@
                             const std::string& uri,
                             const std::string& body) const
   {
-    MemoryBuffer buffer(context_);
+    MemoryBuffer buffer;
 
     if (DoPost(buffer, index, uri, body))
     {
@@ -1581,7 +1669,7 @@
                             const std::string& uri,
                             const std::string& body) const
   {
-    MemoryBuffer buffer(context_);
+    MemoryBuffer buffer;
 
     if (DoPost(buffer, name, uri, body))
     {
@@ -1608,7 +1696,7 @@
     OrthancPluginMemoryBuffer answer;
     uint16_t status;
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
-        (context_, &answer, NULL, &status, peers_,
+        (GetGlobalContext(), &answer, NULL, &status, peers_,
          static_cast<uint32_t>(index), OrthancPluginHttpMethod_Post, uri.c_str(),
          0, NULL, NULL, body.empty() ? NULL : body.c_str(), body.size(), timeout_);
 
@@ -1636,13 +1724,13 @@
     OrthancPluginMemoryBuffer answer;
     uint16_t status;
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
-        (context_, &answer, NULL, &status, peers_,
+        (GetGlobalContext(), &answer, NULL, &status, peers_,
          static_cast<uint32_t>(index), OrthancPluginHttpMethod_Put, uri.c_str(),
          0, NULL, NULL, body.empty() ? NULL : body.c_str(), body.size(), timeout_);
 
     if (code == OrthancPluginErrorCode_Success)
     {
-      OrthancPluginFreeMemoryBuffer(context_, &answer);
+      OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &answer);
       return (status == 200);
     }
     else
@@ -1673,13 +1761,13 @@
     OrthancPluginMemoryBuffer answer;
     uint16_t status;
     OrthancPluginErrorCode code = OrthancPluginCallPeerApi
-        (context_, &answer, NULL, &status, peers_,
+        (GetGlobalContext(), &answer, NULL, &status, peers_,
          static_cast<uint32_t>(index), OrthancPluginHttpMethod_Put, uri.c_str(),
          0, NULL, NULL, NULL, 0, timeout_);
 
     if (code == OrthancPluginErrorCode_Success)
     {
-      OrthancPluginFreeMemoryBuffer(context_, &answer);
+      OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &answer);
       return (status == 200);
     }
     else
@@ -1888,8 +1976,7 @@
   }
 
 
-  OrthancPluginJob* OrthancJob::Create(OrthancPluginContext* context,
-                                       OrthancJob* job)
+  OrthancPluginJob* OrthancJob::Create(OrthancJob* job)
   {
     if (job == NULL)
     {
@@ -1897,9 +1984,9 @@
     }
 
     OrthancPluginJob* orthanc = OrthancPluginCreateJob(
-          context, job, CallbackFinalize, job->jobType_.c_str(),
-          CallbackGetProgress, CallbackGetContent, CallbackGetSerialized,
-          CallbackStep, CallbackStop, CallbackReset);
+      GetGlobalContext(), job, CallbackFinalize, job->jobType_.c_str(),
+      CallbackGetProgress, CallbackGetContent, CallbackGetSerialized,
+      CallbackStep, CallbackStop, CallbackReset);
 
     if (orthanc == NULL)
     {
@@ -1912,25 +1999,24 @@
   }
 
   
-  std::string OrthancJob::Submit(OrthancPluginContext* context,
-                                 OrthancJob* job,
+  std::string OrthancJob::Submit(OrthancJob* job,
                                  int priority)
   {
-    OrthancPluginJob* orthanc = Create(context, job);
+    OrthancPluginJob* orthanc = Create(job);
     
-    char* id = OrthancPluginSubmitJob(context, orthanc, priority);
+    char* id = OrthancPluginSubmitJob(GetGlobalContext(), orthanc, priority);
 
     if (id == NULL)
     {
-      OrthancPluginLogError(context, "Plugin cannot submit job");
-      OrthancPluginFreeJob(context, orthanc);
+      LogError("Plugin cannot submit job");
+      OrthancPluginFreeJob(GetGlobalContext(), orthanc);
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
     }
     else
     {
       std::string tmp(id);
       tmp.assign(id);
-      OrthancPluginFreeString(context, id);
+      OrthancPluginFreeString(GetGlobalContext(), id);
 
       return tmp;
     }
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Thu Dec 06 15:58:08 2018 +0100
@@ -20,7 +20,7 @@
  * 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
@@ -48,10 +48,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
 
 
@@ -71,6 +71,12 @@
 #  define HAS_ORTHANC_PLUGIN_JOB    0
 #endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 4, 3)
+#  define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS  1
+#else
+#  define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS  0
+#endif
+
 
 
 namespace OrthancPlugins
@@ -79,13 +85,20 @@
                                 const char* url,
                                 const OrthancPluginHttpRequest* request);
 
+
+  void SetGlobalContext(OrthancPluginContext* context);
+
+  bool HasGlobalContext();
+
+  OrthancPluginContext* GetGlobalContext();
+
+  
   class OrthancImage;
 
 
   class MemoryBuffer : public boost::noncopyable
   {
   private:
-    OrthancPluginContext*      context_;
     OrthancPluginMemoryBuffer  buffer_;
 
     void Check(OrthancPluginErrorCode code);
@@ -93,7 +106,7 @@
     bool CheckHttp(OrthancPluginErrorCode code);
 
   public:
-    MemoryBuffer(OrthancPluginContext* context);
+    MemoryBuffer();
 
     ~MemoryBuffer()
     {
@@ -127,6 +140,11 @@
       return buffer_.size;
     }
 
+    bool IsEmpty() const
+    {
+      return GetSize() == 0 || GetData() == NULL;
+    }
+
     void Clear();
 
     void ToString(std::string& target) const;
@@ -187,12 +205,12 @@
     bool HttpGet(const std::string& url,
                  const std::string& username,
                  const std::string& password);
- 
+
     bool HttpPost(const std::string& url,
                   const std::string& body,
                   const std::string& username,
                   const std::string& password);
- 
+
     bool HttpPut(const std::string& url,
                  const std::string& body,
                  const std::string& username,
@@ -205,14 +223,12 @@
   class OrthancString : public boost::noncopyable
   {
   private:
-    OrthancPluginContext*  context_;
-    char*                  str_;
+    char*   str_;
 
     void Clear();
 
   public:
-    OrthancString(OrthancPluginContext* context) :
-      context_(context),
+    OrthancString() :
       str_(NULL)
     {
     }
@@ -240,20 +256,13 @@
   class OrthancConfiguration : public boost::noncopyable
   {
   private:
-    OrthancPluginContext*  context_;
-    Json::Value            configuration_;  // Necessarily a Json::objectValue
-    std::string            path_;
+    Json::Value  configuration_;  // Necessarily a Json::objectValue
+    std::string  path_;
 
     std::string GetPath(const std::string& key) const;
 
   public:
-    OrthancConfiguration() : context_(NULL)
-    {
-    }
-
-    OrthancConfiguration(OrthancPluginContext* context);
-
-    OrthancPluginContext* GetContext() const;
+    OrthancConfiguration();
 
     const Json::Value& GetJson() const
     {
@@ -310,7 +319,6 @@
   class OrthancImage : public boost::noncopyable
   {
   private:
-    OrthancPluginContext*  context_;
     OrthancPluginImage*    image_;
 
     void Clear();
@@ -318,18 +326,15 @@
     void CheckImageAvailable();
 
   public:
-    OrthancImage(OrthancPluginContext*  context);
+    OrthancImage();
 
-    OrthancImage(OrthancPluginContext*  context,
-                 OrthancPluginImage*    image);
+    OrthancImage(OrthancPluginImage*    image);
 
-    OrthancImage(OrthancPluginContext*     context,
-                 OrthancPluginPixelFormat  format,
+    OrthancImage(OrthancPluginPixelFormat  format,
                  uint32_t                  width,
                  uint32_t                  height);
 
-    OrthancImage(OrthancPluginContext*     context,
-                 OrthancPluginPixelFormat  format,
+    OrthancImage(OrthancPluginPixelFormat  format,
                  uint32_t                  width,
                  uint32_t                  height,
                  uint32_t                  pitch,
@@ -382,29 +387,24 @@
   class FindMatcher : public boost::noncopyable
   {
   private:
-    OrthancPluginContext*              context_;
     OrthancPluginFindMatcher*          matcher_;
     const OrthancPluginWorklistQuery*  worklist_;
 
-    void SetupDicom(OrthancPluginContext*  context,
-                    const void*            query,
+    void SetupDicom(const void*            query,
                     uint32_t               size);
 
   public:
-    FindMatcher(OrthancPluginContext*              context,
-                const OrthancPluginWorklistQuery*  worklist);
+    FindMatcher(const OrthancPluginWorklistQuery*  worklist);
 
-    FindMatcher(OrthancPluginContext*  context,
-                const void*            query,
+    FindMatcher(const void*            query,
                 uint32_t               size)
     {
-      SetupDicom(context, query, size);
+      SetupDicom(query, size);
     }
 
-    FindMatcher(OrthancPluginContext*  context,
-                const MemoryBuffer&    dicom)
+    FindMatcher(const MemoryBuffer&    dicom)
     {
-      SetupDicom(context, dicom.GetData(), dicom.GetSize());
+      SetupDicom(dicom.GetData(), dicom.GetSize());
     }
 
     ~FindMatcher();
@@ -421,99 +421,91 @@
 
 
   bool RestApiGet(Json::Value& result,
-                  OrthancPluginContext* context,
                   const std::string& uri,
                   bool applyPlugins);
 
+  bool RestApiGetString(std::string& result,
+                        const std::string& uri,
+                        bool applyPlugins);
+
   bool RestApiPost(Json::Value& result,
-                   OrthancPluginContext* context,
                    const std::string& uri,
                    const char* body,
                    size_t bodySize,
                    bool applyPlugins);
 
   bool RestApiPost(Json::Value& result,
-                   OrthancPluginContext* context,
                    const std::string& uri,
                    const Json::Value& body,
                    bool applyPlugins);
 
   inline bool RestApiPost(Json::Value& result,
-                          OrthancPluginContext* context,
                           const std::string& uri,
                           const std::string& body,
                           bool applyPlugins)
   {
-    return RestApiPost(result, context, uri, body.empty() ? NULL : body.c_str(), 
+    return RestApiPost(result, uri, body.empty() ? NULL : body.c_str(),
                        body.size(), applyPlugins);
   }
 
+  inline bool RestApiPost(Json::Value& result,
+                          const std::string& uri,
+                          const MemoryBuffer& body,
+                          bool applyPlugins)
+  {
+    return RestApiPost(result, uri, body.GetData(),
+                       body.GetSize(), applyPlugins);
+  }
+
   bool RestApiPut(Json::Value& result,
-                  OrthancPluginContext* context,
                   const std::string& uri,
                   const char* body,
                   size_t bodySize,
                   bool applyPlugins);
 
   bool RestApiPut(Json::Value& result,
-                  OrthancPluginContext* context,
                   const std::string& uri,
                   const Json::Value& body,
                   bool applyPlugins);
 
   inline bool RestApiPut(Json::Value& result,
-                         OrthancPluginContext* context,
                          const std::string& uri,
                          const std::string& body,
                          bool applyPlugins)
   {
-    return RestApiPut(result, context, uri, body.empty() ? NULL : body.c_str(), 
+    return RestApiPut(result, uri, body.empty() ? NULL : body.c_str(),
                       body.size(), applyPlugins);
   }
 
-  bool RestApiDelete(OrthancPluginContext* context,
-                     const std::string& uri,
+  bool RestApiDelete(const std::string& uri,
                      bool applyPlugins);
 
-  bool HttpDelete(OrthancPluginContext* context,
-                  const std::string& url,
+  bool HttpDelete(const std::string& url,
                   const std::string& username,
                   const std::string& password);
 
-  inline void LogError(OrthancPluginContext* context,
-                       const std::string& message)
-  {
-    if (context != NULL)
-    {
-      OrthancPluginLogError(context, message.c_str());
-    }
-  }
+  void AnswerJson(const Json::Value& value,
+                  OrthancPluginRestOutput* output);
+
+  void AnswerHttpError(uint16_t httpError,
+                       OrthancPluginRestOutput* output);
+
+  void AnswerMethodNotAllowed(OrthancPluginRestOutput* output, const char* allowedMethods);
 
-  inline void LogWarning(OrthancPluginContext* context,
-                         const std::string& message)
-  {
-    if (context != NULL)
-    {
-      OrthancPluginLogWarning(context, message.c_str());
-    }
-  }
+  const char* GetMimeType(const std::string& path);
+
+
+  void LogError(const std::string& message);
 
-  inline void LogInfo(OrthancPluginContext* context,
-                      const std::string& message)
-  {
-    if (context != NULL)
-    {
-      OrthancPluginLogInfo(context, message.c_str());
-    }
-  }
+  void LogWarning(const std::string& message);
 
-  void ReportMinimalOrthancVersion(OrthancPluginContext* context,
-                                   unsigned int major,
+  void LogInfo(const std::string& message);
+
+  void ReportMinimalOrthancVersion(unsigned int major,
                                    unsigned int minor,
                                    unsigned int revision);
   
-  bool CheckMinimalOrthancVersion(OrthancPluginContext* context,
-                                  unsigned int major,
+  bool CheckMinimalOrthancVersion(unsigned int major,
                                   unsigned int minor,
                                   unsigned int revision);
 
@@ -532,6 +524,18 @@
       }
       catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
       {
+#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1
+        if (HasGlobalContext() &&
+            e.HasDetails())
+        {
+          // The "false" instructs Orthanc not to log the detailed
+          // error message. This is to avoid duplicating the details,
+          // because "OrthancException" already does it on construction.
+          OrthancPluginSetHttpErrorDetails
+              (GetGlobalContext(), output, e.GetDetails(), false);
+        }
+#endif
+
         return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
       }
       catch (boost::bad_lexical_cast&)
@@ -547,17 +551,18 @@
 
   
   template <RestCallback Callback>
-  void RegisterRestCallback(OrthancPluginContext* context,
-                            const std::string& uri,
+  void RegisterRestCallback(const std::string& uri,
                             bool isThreadSafe)
   {
     if (isThreadSafe)
     {
-      OrthancPluginRegisterRestCallbackNoLock(context, uri.c_str(), Internals::Protect<Callback>);
+      OrthancPluginRegisterRestCallbackNoLock
+          (GetGlobalContext(), uri.c_str(), Internals::Protect<Callback>);
     }
     else
     {
-      OrthancPluginRegisterRestCallback(context, uri.c_str(), Internals::Protect<Callback>);
+      OrthancPluginRegisterRestCallback
+          (GetGlobalContext(), uri.c_str(), Internals::Protect<Callback>);
     }
   }
 
@@ -568,7 +573,6 @@
   private:
     typedef std::map<std::string, uint32_t>   Index;
 
-    OrthancPluginContext *context_;
     OrthancPluginPeers   *peers_;
     Index                 index_;
     uint32_t              timeout_;
@@ -576,7 +580,7 @@
     size_t GetPeerIndex(const std::string& name) const;
 
   public:
-    OrthancPeers(OrthancPluginContext* context);
+    OrthancPeers();
 
     ~OrthancPeers();
 
@@ -623,11 +627,11 @@
     bool DoGet(Json::Value& target,
                size_t index,
                const std::string& uri) const;
-      
+
     bool DoGet(Json::Value& target,
                const std::string& name,
                const std::string& uri) const;
-      
+
     bool DoPost(MemoryBuffer& target,
                 size_t index,
                 const std::string& uri,
@@ -642,7 +646,7 @@
                 size_t index,
                 const std::string& uri,
                 const std::string& body) const;
-      
+
     bool DoPost(Json::Value& target,
                 const std::string& name,
                 const std::string& uri,
@@ -715,11 +719,9 @@
     
     virtual void Reset() = 0;
 
-    static OrthancPluginJob* Create(OrthancPluginContext* context,
-                                    OrthancJob* job /* takes ownership */);
+    static OrthancPluginJob* Create(OrthancJob* job /* takes ownership */);
 
-    static std::string Submit(OrthancPluginContext* context,
-                              OrthancJob* job /* takes ownership */,
+    static std::string Submit(OrthancJob* job /* takes ownership */,
                               int priority);
   };
 #endif
--- a/Plugins/Samples/ModalityWorklists/Plugin.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Samples/ModalityWorklists/Plugin.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -28,7 +28,6 @@
 #include <iostream>
 #include <algorithm>
 
-static OrthancPluginContext* context_ = NULL;
 static std::string folder_;
 static bool filterIssuerAet_ = false;
 
@@ -40,18 +39,18 @@
                            const OrthancPlugins::FindMatcher& matcher,
                            const std::string& path)
 {
-  OrthancPlugins::MemoryBuffer dicom(context_);
+  OrthancPlugins::MemoryBuffer dicom;
   dicom.ReadFile(path);
 
   if (matcher.IsMatch(dicom))
   {
     // This DICOM file matches the worklist query, add it to the answers
     OrthancPluginErrorCode code = OrthancPluginWorklistAddAnswer
-      (context_, answers, query, dicom.GetData(), dicom.GetSize());
+      (OrthancPlugins::GetGlobalContext(), answers, query, dicom.GetData(), dicom.GetSize());
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      OrthancPlugins::LogError(context_, "Error while adding an answer to a worklist request");
+      OrthancPlugins::LogError("Error while adding an answer to a worklist request");
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
     }
 
@@ -66,7 +65,7 @@
                                                   const char*                       issuerAet)
 {
   // Extract the DICOM instance underlying the C-Find query
-  OrthancPlugins::MemoryBuffer dicom(context_);
+  OrthancPlugins::MemoryBuffer dicom;
   dicom.GetDicomQuery(query);
 
   // Convert the DICOM as JSON, and dump it to the user in "--verbose" mode
@@ -74,12 +73,12 @@
   dicom.DicomToJson(json, OrthancPluginDicomToJsonFormat_Short,
                     static_cast<OrthancPluginDicomToJsonFlags>(0), 0);
 
-  OrthancPlugins::LogInfo(context_, "Received worklist query from remote modality " +
+  OrthancPlugins::LogInfo("Received worklist query from remote modality " +
                           std::string(issuerAet) + ":\n" + json.toStyledString());
 
   if (!filterIssuerAet_)
   {
-    return new OrthancPlugins::FindMatcher(context_, query);
+    return new OrthancPlugins::FindMatcher(query);
   }
   else
   {
@@ -126,9 +125,10 @@
     }
 
     // Encode the modified JSON as a DICOM instance, then convert it to a C-Find matcher
-    OrthancPlugins::MemoryBuffer modified(context_);
+    OrthancPlugins::MemoryBuffer modified;
     modified.CreateDicom(json, OrthancPluginCreateDicomFlags_None);
-    return new OrthancPlugins::FindMatcher(context_, modified);
+    
+    return new OrthancPlugins::FindMatcher(modified);
   }
 }
 
@@ -170,7 +170,7 @@
             // We found a worklist (i.e. a DICOM find with extension ".wl"), match it against the query
             if (MatchWorklist(answers, query, *matcher, it->path().string()))
             {
-              OrthancPlugins::LogInfo(context_, "Worklist matched: " + it->path().string());
+              OrthancPlugins::LogInfo("Worklist matched: " + it->path().string());
               matchedWorklistCount++;
             }
           }
@@ -179,17 +179,17 @@
 
       std::ostringstream message;
       message << "Worklist C-Find: parsed " << parsedFilesCount << " files, found " << matchedWorklistCount << " match(es)";
-      OrthancPlugins::LogInfo(context_, message.str());
+      OrthancPlugins::LogInfo(message.str());
 
     }
     catch (fs::filesystem_error&)
     {
-      OrthancPlugins::LogError(context_, "Inexistent folder while scanning for worklists: " + source.string());
+      OrthancPlugins::LogError("Inexistent folder while scanning for worklists: " + source.string());
       return OrthancPluginErrorCode_DirectoryExpected;
     }
 
     // Uncomment the following line if too many answers are to be returned
-    // OrthancPluginMarkWorklistAnswersIncomplete(context_, answers);
+    // OrthancPluginMarkWorklistAnswersIncomplete(OrthancPlugins::GetGlobalContext(), answers);
 
     return OrthancPluginErrorCode_Success;
   }
@@ -204,22 +204,21 @@
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
   {
-    context_ = c;
+    OrthancPlugins::SetGlobalContext(c);
 
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(c) == 0)
     {
-      OrthancPlugins::ReportMinimalOrthancVersion(context_,
-                                                  ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
                                                   ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
                                                   ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
       return -1;
     }
 
-    OrthancPlugins::LogWarning(context_, "Sample worklist plugin is initializing");
-    OrthancPluginSetDescription(context_, "Serve DICOM modality worklists from a folder with Orthanc.");
+    OrthancPlugins::LogWarning("Sample worklist plugin is initializing");
+    OrthancPluginSetDescription(c, "Serve DICOM modality worklists from a folder with Orthanc.");
 
-    OrthancPlugins::OrthancConfiguration configuration(context_);
+    OrthancPlugins::OrthancConfiguration configuration;
 
     OrthancPlugins::OrthancConfiguration worklists;
     configuration.GetSection(worklists, "Worklists");
@@ -229,12 +228,12 @@
     {
       if (worklists.LookupStringValue(folder_, "Database"))
       {
-        OrthancPlugins::LogWarning(context_, "The database of worklists will be read from folder: " + folder_);
-        OrthancPluginRegisterWorklistCallback(context_, Callback);
+        OrthancPlugins::LogWarning("The database of worklists will be read from folder: " + folder_);
+        OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), Callback);
       }
       else
       {
-        OrthancPlugins::LogError(context_, "The configuration option \"Worklists.Database\" must contain a path");
+        OrthancPlugins::LogError("The configuration option \"Worklists.Database\" must contain a path");
         return -1;
       }
 
@@ -242,7 +241,7 @@
     }
     else
     {
-      OrthancPlugins::LogWarning(context_, "Worklist server is disabled by the configuration file");
+      OrthancPlugins::LogWarning("Worklist server is disabled by the configuration file");
     }
 
     return 0;
@@ -251,7 +250,7 @@
 
   ORTHANC_PLUGINS_API void OrthancPluginFinalize()
   {
-    OrthancPluginLogWarning(context_, "Sample worklist plugin is finalizing");
+    OrthancPlugins::LogWarning("Sample worklist plugin is finalizing");
   }
 
 
--- a/Plugins/Samples/ServeFolders/Plugin.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/Plugins/Samples/ServeFolders/Plugin.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -33,7 +33,6 @@
 
 
 
-static OrthancPluginContext* context_ = NULL;
 static std::map<std::string, std::string> extensions_;
 static std::map<std::string, std::string> folders_;
 static const char* INDEX_URI = "/app/plugin-serve-folders.html";
@@ -46,9 +45,10 @@
   if (!allowCache_)
   {
     // http://stackoverflow.com/a/2068407/881731
-    OrthancPluginSetHttpHeader(context_, output, "Cache-Control", "no-cache, no-store, must-revalidate");
-    OrthancPluginSetHttpHeader(context_, output, "Pragma", "no-cache");
-    OrthancPluginSetHttpHeader(context_, output, "Expires", "0");
+    OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
+    OrthancPluginSetHttpHeader(context, output, "Cache-Control", "no-cache, no-store, must-revalidate");
+    OrthancPluginSetHttpHeader(context, output, "Pragma", "no-cache");
+    OrthancPluginSetHttpHeader(context, output, "Expires", "0");
   }
 }
 
@@ -89,7 +89,7 @@
   }
   else
   {
-    OrthancPlugins::LogWarning(context_, "ServeFolders: Unknown MIME type for extension \"" + extension + "\"");
+    OrthancPlugins::LogWarning("ServeFolders: Unknown MIME type for extension \"" + extension + "\"");
     return "application/octet-stream";
   }
 }
@@ -104,8 +104,8 @@
   std::map<std::string, std::string>::const_iterator found = folders_.find(uri);
   if (found == folders_.end())
   {
-    OrthancPlugins::LogError(context_, "Unknown URI in plugin server-folders: " + uri);
-    OrthancPluginSendHttpStatusCode(context_, output, 404);
+    OrthancPlugins::LogError("Unknown URI in plugin server-folders: " + uri);
+    OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 404);
     return false;
   }
   else
@@ -123,15 +123,15 @@
 {
   if (generateETag_)
   {
-    OrthancPlugins::OrthancString md5(context_);
-    md5.Assign(OrthancPluginComputeMd5(context_, content, size));
+    OrthancPlugins::OrthancString md5;
+    md5.Assign(OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), content, size));
 
     std::string etag = "\"" + std::string(md5.GetContent()) + "\"";
-    OrthancPluginSetHttpHeader(context_, output, "ETag", etag.c_str());
+    OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "ETag", etag.c_str());
   }
 
   SetHttpHeaders(output);
-  OrthancPluginAnswerBuffer(context_, output, content, size, mime.c_str());
+  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, content, size, mime.c_str());
 }
 
 
@@ -143,7 +143,7 @@
 
   if (request->method != OrthancPluginHttpMethod_Get)
   {
-    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET");
     return;
   }
 
@@ -198,7 +198,7 @@
       std::string path = folder + "/" + item.string();
       std::string mime = GetMimeType(path);
 
-      OrthancPlugins::MemoryBuffer content(context_);
+      OrthancPlugins::MemoryBuffer content;
 
       try
       {
@@ -209,9 +209,11 @@
         ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentFile);
       }
 
-      boost::posix_time::ptime lastModification = boost::posix_time::from_time_t(fs::last_write_time(path));
+      boost::posix_time::ptime lastModification =
+        boost::posix_time::from_time_t(fs::last_write_time(path));
       std::string t = boost::posix_time::to_iso_string(lastModification);
-      OrthancPluginSetHttpHeader(context_, output, "Last-Modified", t.c_str());
+      OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(),
+                                 output, "Last-Modified", t.c_str());
 
       Answer(output, content.GetData(), content.GetSize(), mime);
     }
@@ -225,7 +227,7 @@
 {
   if (request->method != OrthancPluginHttpMethod_Get)
   {
-    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET");
     return;
   }
 
@@ -258,7 +260,7 @@
 {
   if (folders.type() != Json::objectValue)
   {
-    OrthancPlugins::LogError(context_, "The list of folders to be served is badly formatted (must be a JSON object)");
+    OrthancPlugins::LogError("The list of folders to be served is badly formatted (must be a JSON object)");
     ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
   }
 
@@ -270,7 +272,7 @@
   {
     if (folders[*it].type() != Json::stringValue)
     {
-      OrthancPlugins::LogError(context_, "The folder to be served \"" + *it + 
+      OrthancPlugins::LogError("The folder to be served \"" + *it + 
                                "\" must be associated with a string value (its mapped URI)");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -292,7 +294,7 @@
 
     if (baseUri.empty())
     {
-      OrthancPlugins::LogError(context_, "The URI of a folder to be served cannot be empty");
+      OrthancPlugins::LogError("The URI of a folder to be served cannot be empty");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
 
@@ -300,7 +302,7 @@
     const std::string folder = folders[*it].asString();
     if (!boost::filesystem::is_directory(folder))
     {
-      OrthancPlugins::LogError(context_, "Trying and serve an inexistent folder: " + folder);
+      OrthancPlugins::LogError("Trying and serve an inexistent folder: " + folder);
       ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentFile);
     }
 
@@ -309,7 +311,7 @@
     // Register the callback to serve the folder
     {
       const std::string regex = "/(" + baseUri + ")/(.*)";
-      OrthancPlugins::RegisterRestCallback<ServeFolder>(context_, regex.c_str(), true);
+      OrthancPlugins::RegisterRestCallback<ServeFolder>(regex.c_str(), true);
     }
   }
 }
@@ -319,7 +321,7 @@
 {
   if (extensions.type() != Json::objectValue)
   {
-    OrthancPlugins::LogError(context_, "The list of extensions is badly formatted (must be a JSON object)");
+    OrthancPlugins::LogError("The list of extensions is badly formatted (must be a JSON object)");
     ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
   }
 
@@ -330,7 +332,7 @@
   {
     if (extensions[*it].type() != Json::stringValue)
     {
-      OrthancPlugins::LogError(context_, "The file extension \"" + *it + 
+      OrthancPlugins::LogError("The file extension \"" + *it + 
                                "\" must be associated with a string value (its MIME type)");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -349,11 +351,12 @@
 
     if (mime.empty())
     {
-      OrthancPlugins::LogWarning(context_, "ServeFolders: Removing MIME type for file extension \"." + name + "\"");
+      OrthancPlugins::LogWarning("ServeFolders: Removing MIME type for file extension \"." +
+                                 name + "\"");
     }
     else
     {
-      OrthancPlugins::LogWarning(context_, "ServeFolders: Associating file extension \"." + name + 
+      OrthancPlugins::LogWarning("ServeFolders: Associating file extension \"." + name + 
                                  "\" with MIME type \"" + mime + "\"");
     }
   }  
@@ -365,7 +368,7 @@
   OrthancPlugins::OrthancConfiguration configuration;
 
   {
-    OrthancPlugins::OrthancConfiguration globalConfiguration(context_);
+    OrthancPlugins::OrthancConfiguration globalConfiguration;
     globalConfiguration.GetSection(configuration, "ServeFolders");
   }
 
@@ -384,7 +387,7 @@
     if (configuration.LookupBooleanValue(tmp, "AllowCache"))
     {
       allowCache_ = tmp;
-      OrthancPlugins::LogWarning(context_, "ServeFolders: Requesting the HTTP client to " +
+      OrthancPlugins::LogWarning("ServeFolders: Requesting the HTTP client to " +
                                  std::string(tmp ? "enable" : "disable") + 
                                  " its caching mechanism");
     }
@@ -392,7 +395,8 @@
     if (configuration.LookupBooleanValue(tmp, "GenerateETag"))
     {
       generateETag_ = tmp;
-      OrthancPlugins::LogWarning(context_, "ServeFolders: The computation of an ETag for the served resources is " +
+      OrthancPlugins::LogWarning("ServeFolders: The computation of an ETag for the "
+                                 "served resources is " +
                                  std::string(tmp ? "enabled" : "disabled"));
     }
 
@@ -403,7 +407,8 @@
 
   if (folders_.empty())
   {
-    OrthancPlugins::LogWarning(context_, "ServeFolders: Empty configuration file: No additional folder will be served!");
+    OrthancPlugins::LogWarning("ServeFolders: Empty configuration file: "
+                               "No additional folder will be served!");
   }
 }
 
@@ -412,22 +417,21 @@
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
   {
-    context_ = context;
+    OrthancPlugins::SetGlobalContext(context);
 
     /* Check the version of the Orthanc core */
-    if (OrthancPluginCheckVersion(context_) == 0)
+    if (OrthancPluginCheckVersion(context) == 0)
     {
-      OrthancPlugins::ReportMinimalOrthancVersion(context_, 
-                                                  ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
                                                   ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
                                                   ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
       return -1;
     }
 
     RegisterDefaultExtensions();
-    OrthancPluginSetDescription(context_, "Serve additional folders with the HTTP server of Orthanc.");
+    OrthancPluginSetDescription(context, "Serve additional folders with the HTTP server of Orthanc.");
     OrthancPluginSetRootUri(context, INDEX_URI);
-    OrthancPlugins::RegisterRestCallback<ListServedFolders>(context_, INDEX_URI, true);
+    OrthancPlugins::RegisterRestCallback<ListServedFolders>(INDEX_URI, true);
 
     try
     {
@@ -435,8 +439,8 @@
     }
     catch (OrthancPlugins::PluginException& e)
     {
-      OrthancPlugins::LogError(context_, "Error while initializing the ServeFolders plugin: " + 
-                               std::string(e.What(context_)));
+      OrthancPlugins::LogError("Error while initializing the ServeFolders plugin: " + 
+                               std::string(e.What(context)));
     }
 
     return 0;
--- a/Resources/CMake/BoostConfiguration.cmake	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/CMake/BoostConfiguration.cmake	Thu Dec 06 15:58:08 2018 +0100
@@ -53,10 +53,10 @@
   ## Parameters for static compilation of Boost 
   ##
   
-  set(BOOST_NAME boost_1_67_0)
-  set(BOOST_VERSION 1.67.0)
-  set(BOOST_BCP_SUFFIX bcpdigest-1.4.0)
-  set(BOOST_MD5 "fb3535a88e72c3d4c4d06b047b8e57fe")
+  set(BOOST_NAME boost_1_68_0)
+  set(BOOST_VERSION 1.68.0)
+  set(BOOST_BCP_SUFFIX bcpdigest-1.4.3)
+  set(BOOST_MD5 "2d272566a72343766c523e2e32313c65")
   set(BOOST_URL "http://www.orthanc-server.com/downloads/third-party/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz")
   set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME})
 
@@ -94,7 +94,7 @@
   endif()
 
   include_directories(
-    ${BOOST_SOURCES_DIR}
+    BEFORE ${BOOST_SOURCES_DIR}
     )
 
   add_definitions(
--- a/Resources/CMake/BoostConfiguration.sh	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/CMake/BoostConfiguration.sh	Thu Dec 06 15:58:08 2018 +0100
@@ -19,10 +19,11 @@
 ##   - Orthanc 1.3.0: Boost 1.64.0
 ##   - Orthanc 1.3.1: Boost 1.65.1
 ##   - Orthanc 1.3.2: Boost 1.66.0
-##   - Orthanc >= 1.4.0: Boost 1.67.0
+##   - Orthanc between 1.4.0 and 1.4.2: Boost 1.67.0
+##   - Orthanc >= 1.4.3: Boost 1.68.0
 
-BOOST_VERSION=1_67_0
-ORTHANC_VERSION=1.4.0
+BOOST_VERSION=1_68_0
+ORTHANC_VERSION=1.4.3
 
 rm -rf /tmp/boost_${BOOST_VERSION}
 rm -rf /tmp/bcp/boost_${BOOST_VERSION}
--- a/Resources/CMake/DcmtkConfiguration.cmake	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/CMake/DcmtkConfiguration.cmake	Thu Dec 06 15:58:08 2018 +0100
@@ -324,6 +324,18 @@
     -DDCMTK_VERSION_NUMBER=${DCMTK_VERSION_NUMBER}
     )
 
+  if (NOT ENABLE_DCMTK_LOG)
+    # Disable logging internal to DCMTK
+    # https://groups.google.com/d/msg/orthanc-users/v2SzzAmY948/VxT1QVGiBAAJ
+    add_definitions(
+      -DDCMTK_LOG4CPLUS_DISABLE_FATAL=1
+      -DDCMTK_LOG4CPLUS_DISABLE_ERROR=1
+      -DDCMTK_LOG4CPLUS_DISABLE_WARN=1
+      -DDCMTK_LOG4CPLUS_DISABLE_INFO=1
+      -DDCMTK_LOG4CPLUS_DISABLE_DEBUG=1
+      )
+  endif()
+
   include_directories(
     #${DCMTK_SOURCES_DIR}
     ${DCMTK_SOURCES_DIR}/config/include
--- a/Resources/CMake/LuaConfiguration.cmake	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/CMake/LuaConfiguration.cmake	Thu Dec 06 15:58:08 2018 +0100
@@ -1,7 +1,7 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_LUA)
-  SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.1.5)
-  SET(LUA_MD5 "2e115fe26e435e33b0d5c022e4490567")
-  SET(LUA_URL "http://www.orthanc-server.com/downloads/third-party/lua-5.1.5.tar.gz")
+  SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.3.5)
+  SET(LUA_MD5 "4f4b4f323fd3514a68e0ab3da8ce3455")
+  SET(LUA_URL "http://www.orthanc-server.com/downloads/third-party/lua-5.3.5.tar.gz")
 
   DownloadPackage(${LUA_MD5} ${LUA_URL} "${LUA_SOURCES_DIR}")
 
@@ -18,53 +18,83 @@
       # shared libraries can call them
       set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--export-dynamic")
 
+      if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
+          ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
+        add_definitions(-DLUA_USE_LINUX=1)
+      elseif (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
+        add_definitions(
+          -DLUA_USE_LINUX=1
+          -DLUA_USE_READLINE=1
+          )
+      elseif (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+        add_definitions(-DLUA_USE_POSIX=1)
+      endif()
+
     elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-      add_definitions(-DLUA_DL_DLL=1)       # Enable loading of shared libraries (for Microsoft Windows)
+      add_definitions(
+        -DLUA_DL_DLL=1       # Enable loading of shared libraries (for Microsoft Windows)
+        )
       
     elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
-      add_definitions(-LUA_DL_DYLD=1)       # Enable loading of shared libraries (for Apple OS X)
+      add_definitions(
+        -DLUA_USE_MACOSX=1
+        -DLUA_DL_DYLD=1       # Enable loading of shared libraries (for Apple OS X)
+        )
       
     else()
       message(FATAL_ERROR "Support your platform here")
     endif()
   endif()
 
+  add_definitions(
+    -DLUA_COMPAT_5_2=1
+    )
+
   include_directories(
     ${LUA_SOURCES_DIR}/src
     )
 
   set(LUA_SOURCES
+    # Don't compile the Lua command-line
+    #${LUA_SOURCES_DIR}/src/lua.c
+    #${LUA_SOURCES_DIR}/src/luac.c
+
     # Core Lua
     ${LUA_SOURCES_DIR}/src/lapi.c
-    ${LUA_SOURCES_DIR}/src/lcode.c 
-    ${LUA_SOURCES_DIR}/src/ldebug.c 
-    ${LUA_SOURCES_DIR}/src/ldo.c 
-    ${LUA_SOURCES_DIR}/src/ldump.c 
-    ${LUA_SOURCES_DIR}/src/lfunc.c 
+    ${LUA_SOURCES_DIR}/src/lcode.c
+    ${LUA_SOURCES_DIR}/src/lctype.c
+    ${LUA_SOURCES_DIR}/src/ldebug.c
+    ${LUA_SOURCES_DIR}/src/ldo.c
+    ${LUA_SOURCES_DIR}/src/ldump.c
+    ${LUA_SOURCES_DIR}/src/lfunc.c
     ${LUA_SOURCES_DIR}/src/lgc.c
     ${LUA_SOURCES_DIR}/src/llex.c
-    ${LUA_SOURCES_DIR}/src/lmem.c 
-    ${LUA_SOURCES_DIR}/src/lobject.c 
-    ${LUA_SOURCES_DIR}/src/lopcodes.c 
+    ${LUA_SOURCES_DIR}/src/lmem.c
+    ${LUA_SOURCES_DIR}/src/lobject.c
+    ${LUA_SOURCES_DIR}/src/lopcodes.c
     ${LUA_SOURCES_DIR}/src/lparser.c
-    ${LUA_SOURCES_DIR}/src/lstate.c 
+    ${LUA_SOURCES_DIR}/src/lstate.c
     ${LUA_SOURCES_DIR}/src/lstring.c
     ${LUA_SOURCES_DIR}/src/ltable.c
     ${LUA_SOURCES_DIR}/src/ltm.c
-    ${LUA_SOURCES_DIR}/src/lundump.c 
-    ${LUA_SOURCES_DIR}/src/lvm.c 
+    ${LUA_SOURCES_DIR}/src/lundump.c
+    ${LUA_SOURCES_DIR}/src/lvm.c
     ${LUA_SOURCES_DIR}/src/lzio.c
 
     # Base Lua modules
     ${LUA_SOURCES_DIR}/src/lauxlib.c
     ${LUA_SOURCES_DIR}/src/lbaselib.c
+    ${LUA_SOURCES_DIR}/src/lbitlib.c
+    ${LUA_SOURCES_DIR}/src/lcorolib.c
     ${LUA_SOURCES_DIR}/src/ldblib.c
     ${LUA_SOURCES_DIR}/src/liolib.c
     ${LUA_SOURCES_DIR}/src/lmathlib.c
+    ${LUA_SOURCES_DIR}/src/loadlib.c
     ${LUA_SOURCES_DIR}/src/loslib.c
+    ${LUA_SOURCES_DIR}/src/lstrlib.c
     ${LUA_SOURCES_DIR}/src/ltablib.c
-    ${LUA_SOURCES_DIR}/src/lstrlib.c
-    ${LUA_SOURCES_DIR}/src/loadlib.c
+    ${LUA_SOURCES_DIR}/src/lutf8lib.c
+
     ${LUA_SOURCES_DIR}/src/linit.c
     )
 
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Thu Dec 06 15:58:08 2018 +0100
@@ -125,6 +125,7 @@
   ${ORTHANC_ROOT}/Core/Cache/MemoryCache.cpp
   ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp
   ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp
+  ${ORTHANC_ROOT}/Core/EnumerationDictionary.h
   ${ORTHANC_ROOT}/Core/Enumerations.cpp
   ${ORTHANC_ROOT}/Core/FileStorage/MemoryStorageArea.cpp
   ${ORTHANC_ROOT}/Core/Logging.cpp
--- a/Resources/CMake/OrthancFrameworkParameters.cmake	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/CMake/OrthancFrameworkParameters.cmake	Thu Dec 06 15:58:08 2018 +0100
@@ -59,6 +59,7 @@
 set(USE_DCMTK_360 OFF CACHE BOOL "Use older DCMTK version 3.6.0 in static builds (instead of default 3.6.2)")
 set(USE_DCMTK_362_PRIVATE_DIC ON CACHE BOOL "Use the dictionary of private tags from DCMTK 3.6.2 if using DCMTK 3.6.0")
 set(USE_SYSTEM_DCMTK ON CACHE BOOL "Use the system version of DCMTK")
+set(ENABLE_DCMTK_LOG ON CACHE BOOL "Enable logging internal to DCMTK")
 set(ENABLE_DCMTK_JPEG ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression")
 set(ENABLE_DCMTK_JPEG_LOSSLESS ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression")
 
--- a/Resources/CMake/UuidConfiguration.cmake	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/CMake/UuidConfiguration.cmake	Thu Dec 06 15:58:08 2018 +0100
@@ -5,10 +5,33 @@
     SET(E2FSPROGS_URL "http://www.orthanc-server.com/downloads/third-party/e2fsprogs-1.43.8.tar.gz")
     SET(E2FSPROGS_MD5 "670b7a74a8ead5333acf21b9afc92b3c")
 
+    if (IS_DIRECTORY "${E2FSPROGS_SOURCES_DIR}")
+      set(FirstRun OFF)
+    else()
+      set(FirstRun ON)
+    endif()
+
     DownloadPackage(${E2FSPROGS_MD5} ${E2FSPROGS_URL} "${E2FSPROGS_SOURCES_DIR}")
 
+    
+    ##
+    ## Patch for OS X, in order to be compatible with Cocoa (used in Stone)
+    ## 
+
+    execute_process(
+      COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+      ${ORTHANC_ROOT}/Resources/Patches/e2fsprogs-1.43.8-apple.patch
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      RESULT_VARIABLE Failure
+      )
+
+    if (FirstRun AND Failure)
+      message(FATAL_ERROR "Error while patching a file")
+    endif()
+
+
     include_directories(
-      ${E2FSPROGS_SOURCES_DIR}/lib
+      BEFORE ${E2FSPROGS_SOURCES_DIR}/lib
       )
 
     set(UUID_SOURCES
--- a/Resources/Configuration.json	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/Configuration.json	Thu Dec 06 15:58:08 2018 +0100
@@ -198,6 +198,10 @@
     //}
   },
 
+  // Whether to store the DICOM modalities in the Orthanc database
+  // instead of in this configuration file (new in Orthanc 1.4.3)
+  "DicomModalitiesInDatabase" : false,
+
   // Whether the Orthanc SCP allows incoming C-Echo requests, even
   // from SCU modalities it does not know about (i.e. that are not
   // listed in the "DicomModalities" option above). Orthanc 1.3.0
@@ -248,6 +252,10 @@
     // }
   },
 
+  // Whether to store the Orthanc peers in the Orthanc database
+  // instead of in this configuration file (new in Orthanc 1.4.3)
+  "OrthancPeersInDatabase" : false,
+
   // Parameters of the HTTP proxy to be used by Orthanc. If set to the
   // empty string, no HTTP proxy is used. For instance:
   //   "HttpProxy" : "192.168.0.1:3128"
@@ -273,7 +281,9 @@
   // peers in HTTPS requests. From curl documentation ("--cacert"
   // option): "Tells curl to use the specified certificate file to
   // verify the peers. The file may contain multiple CA
-  // certificates. The certificate(s) must be in PEM format."
+  // certificates. The certificate(s) must be in PEM format." On
+  // Debian-based systems, this option can be set to
+  // "/etc/ssl/certs/ca-certificates.crt"
   "HttpsCACertificates" : "",
 
 
@@ -412,12 +422,17 @@
   // Whether to run DICOM C-Move operations synchronously. If set to
   // "false" (the default), each incoming C-Move request results in
   // creating a new background job. Up to Orthanc 1.3.2, the implicit
-  // behavior was to use synchronous C-Move.
-  "SynchronousCMove" : false,
+  // behavior was to use synchronous C-Move. Between Orthanc 1.4.0 and
+  // 1.4.2, the default behavior was set to asynchronous C-Move. Since
+  // Orthanc 1.4.3, the default behavior is synchronous C-Move
+  // (backward compatibility with Orthanc <= 1.3.2).
+  "SynchronousCMove" : true,
 
   // Maximum number of completed jobs that are kept in memory. A
   // processing job is considered as complete once it is tagged as
-  // "Success" or "Failure".
+  // "Success" or "Failure". Since Orthanc 1.4.3, a value of "0"
+  // indicates to keep no job in memory (i.e. jobs are removed from
+  // the history as soon as they are completed).
   "JobsHistorySize" : 10,
 
   // Specifies how Orthanc reacts when it receives a DICOM instance
@@ -425,5 +440,12 @@
   // instance replaces the old one. If set to "false", the new
   // instance is discarded and the old one is kept. Up to Orthanc
   // 1.4.1, the implicit behavior corresponded to "false".
-  "OverwriteInstances" : false
+  "OverwriteInstances" : false,
+
+  // Maximum number of ZIP/media archives that are maintained by
+  // Orthanc, as a response to the asynchronous creation of archives.
+  // The least recently used archives get deleted as new archives are
+  // generated. This option was introduced in Orthanc 1.4.3, and has
+  // no effect on the synchronous generation of archives.
+  "MediaArchiveSize" : 1
 }
--- a/Resources/DownloadOrthancFramework.cmake	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/DownloadOrthancFramework.cmake	Thu Dec 06 15:58:08 2018 +0100
@@ -87,6 +87,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "d0ccdf68e855d8224331f13774992750")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.0")
         set(ORTHANC_FRAMEWORK_MD5 "81e15f34d97ac32bbd7d26e85698835a")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.1")
+        set(ORTHANC_FRAMEWORK_MD5 "9b6f6114264b17ed421b574cd6476127")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2")
         set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394")
       endif()
--- a/Resources/ErrorCodes.json	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/ErrorCodes.json	Thu Dec 06 15:58:08 2018 +0100
@@ -421,37 +421,44 @@
     "Description": "Cannot store an instance"
   },
   {
-    "Code": 2019, 
+    "Code": 2019,
+    "HttpStatus": 400, 
     "Name": "CreateDicomNotString", 
     "Description": "Only string values are supported when creating DICOM instances"
   },
   {
-    "Code": 2020, 
+    "Code": 2020,
+    "HttpStatus": 400, 
     "Name": "CreateDicomOverrideTag", 
     "Description": "Trying to override a value inherited from a parent module"
   },
   {
-    "Code": 2021, 
+    "Code": 2021,
+    "HttpStatus": 400, 
     "Name": "CreateDicomUseContent", 
     "Description": "Use \\\"Content\\\" to inject an image into a new DICOM instance"
   },
   {
-    "Code": 2022, 
+    "Code": 2022,
+    "HttpStatus": 400, 
     "Name": "CreateDicomNoPayload", 
     "Description": "No payload is present for one instance in the series"
   },
   {
-    "Code": 2023, 
+    "Code": 2023,
+    "HttpStatus": 400, 
     "Name": "CreateDicomUseDataUriScheme", 
     "Description": "The payload of the DICOM instance must be specified according to Data URI scheme"
   },
   {
-    "Code": 2024, 
+    "Code": 2024,
+    "HttpStatus": 400, 
     "Name": "CreateDicomBadParent", 
     "Description": "Trying to attach a new DICOM instance to an inexistent resource"
   },
   {
-    "Code": 2025, 
+    "Code": 2025,
+    "HttpStatus": 400, 
     "Name": "CreateDicomParentIsInstance", 
     "Description": "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)"
   },
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/boost-1.68.0-linux-standard-base.patch	Thu Dec 06 15:58:08 2018 +0100
@@ -0,0 +1,76 @@
+diff -urEb boost_1_68_0.orig/boost/move/adl_move_swap.hpp boost_1_68_0/boost/move/adl_move_swap.hpp
+--- boost_1_68_0.orig/boost/move/adl_move_swap.hpp	2018-11-13 16:08:32.214434915 +0100
++++ boost_1_68_0/boost/move/adl_move_swap.hpp	2018-11-13 16:09:03.558399048 +0100
+@@ -28,6 +28,8 @@
+ //Try to avoid including <algorithm>, as it's quite big
+ #if defined(_MSC_VER) && defined(BOOST_DINKUMWARE_STDLIB)
+    #include <utility>   //Dinkum libraries define std::swap in utility which is lighter than algorithm
++#elif defined(__LSB_VERSION__)
++#  include <utility>
+ #elif defined(BOOST_GNU_STDLIB)
+    //For non-GCC compilers, where GNUC version is not very reliable, or old GCC versions
+    //use the good old stl_algobase header, which is quite lightweight
+diff -urEb boost_1_68_0.orig/boost/thread/detail/config.hpp boost_1_68_0/boost/thread/detail/config.hpp
+--- boost_1_68_0.orig/boost/thread/detail/config.hpp	2018-11-13 16:08:32.210434920 +0100
++++ boost_1_68_0/boost/thread/detail/config.hpp	2018-11-13 16:10:03.386329911 +0100
+@@ -417,7 +417,7 @@
+   #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
+ #elif defined(BOOST_THREAD_CHRONO_MAC_API)
+   #define BOOST_THREAD_HAS_MONO_CLOCK
+-#elif defined(__ANDROID__)
++#elif defined(__LSB_VERSION__) || defined(__ANDROID__)
+   #define BOOST_THREAD_HAS_MONO_CLOCK
+   #if defined(__ANDROID_API__) && __ANDROID_API__ >= 21
+     #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
+diff -urEb boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp
+--- boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp	2018-11-13 16:08:32.206434924 +0100
++++ boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp	2018-11-13 16:11:08.374253901 +0100
+@@ -32,8 +32,11 @@
+ namespace boost {
+ namespace detail {
+ 
++// https://stackoverflow.com/a/15474269
++#ifndef Q_MOC_RUN
+ // This namespace ensures that argument-dependent name lookup does not mess things up.
+ namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) {
++#endif
+ 
+ // 1. a function to have an instance of type T without requiring T to be default
+ // constructible
+@@ -181,7 +184,9 @@
+    BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Lhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value));
+ };
+ 
++#ifndef Q_MOC_RUN
+ } // namespace impl
++#endif
+ } // namespace detail
+ 
+ // this is the accessible definition of the trait to end user
+diff -urEb boost_1_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp
+--- boost_1_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp	2018-11-13 16:08:32.206434924 +0100
++++ boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp	2018-11-13 16:14:30.278012856 +0100
+@@ -45,8 +45,11 @@
+ namespace boost {
+ namespace detail {
+ 
++// https://stackoverflow.com/a/15474269
++#ifndef Q_MOC_RUN
+ // This namespace ensures that argument-dependent name lookup does not mess things up.
+ namespace BOOST_JOIN(BOOST_TT_TRAIT_NAME,_impl) {
++#endif
+ 
+ // 1. a function to have an instance of type T without requiring T to be default
+ // constructible
+@@ -194,7 +197,10 @@
+    BOOST_STATIC_CONSTANT(bool, value = (trait_impl1 < Rhs_noref, Ret, BOOST_TT_FORBIDDEN_IF >::value));
+ };
+ 
++
++#ifndef Q_MOC_RUN
+ } // namespace impl
++#endif
+ } // namespace detail
+ 
+ // this is the accessible definition of the trait to end user
+Only in boost_1_68_0/boost/type_traits/detail: has_prefix_operator.hpp~
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/e2fsprogs-1.43.8-apple.patch	Thu Dec 06 15:58:08 2018 +0100
@@ -0,0 +1,24 @@
+diff -urEb e2fsprogs-1.43.8.orig/lib/uuid/uuid.h.in e2fsprogs-1.43.8/lib/uuid/uuid.h.in
+--- e2fsprogs-1.43.8.orig/lib/uuid/uuid.h.in	2018-01-02 05:52:58.000000000 +0100
++++ e2fsprogs-1.43.8/lib/uuid/uuid.h.in	2018-11-05 12:18:29.962235770 +0100
+@@ -35,6 +35,20 @@
+ #ifndef _UUID_UUID_H
+ #define _UUID_UUID_H
+ 
++
++#if defined(__APPLE__)
++// This patch defines the "uuid_string_t" type on OS X, which is
++// required if linking against Cocoa (this occurs in Stone of Orthanc)
++#include <sys/_types.h>
++#include <sys/_types/_uuid_t.h>
++
++#ifndef _UUID_STRING_T
++#define _UUID_STRING_T
++typedef __darwin_uuid_string_t  uuid_string_t;
++#endif /* _UUID_STRING_T */
++#endif
++
++
+ #include <sys/types.h>
+ #ifndef _WIN32
+ #include <sys/time.h>
--- a/Resources/Samples/Lua/CallWebService.lua	Thu Oct 18 10:48:11 2018 +0200
+++ b/Resources/Samples/Lua/CallWebService.lua	Thu Dec 06 15:58:08 2018 +0100
@@ -5,7 +5,9 @@
 
 -- Download and install the JSON module for Lua by Jeffrey Friedl
 -- http://regex.info/blog/lua/json
-JSON = (loadstring(HttpGet('http://regex.info/code/JSON.lua'))) ()
+
+-- NOTE : Replace "load" by "loadstring" for Lua <= 5.1
+JSON = (load(HttpGet('http://regex.info/code/JSON.lua'))) ()
 
 SetHttpCredentials('alice', 'alicePassword')
 
--- a/TODO	Thu Oct 18 10:48:11 2018 +0200
+++ b/TODO	Thu Dec 06 15:58:08 2018 +0100
@@ -135,7 +135,6 @@
 * Avoid direct calls to FromDcmtkBridge (make most of its 
   methods private), go through ParsedDicomFile wherever possible
 
-
 =================
 Platform-specific
 =================
--- a/UnitTestsSources/ImageProcessingTests.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/ImageProcessingTests.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -94,12 +94,12 @@
     std::auto_ptr<Image>  image_;
 
   protected:
-    virtual void SetUp() 
+    virtual void SetUp() ORTHANC_OVERRIDE
     {
       image_.reset(new Image(ImageTraits::PixelTraits::GetPixelFormat(), 7, 9, false));
     }
 
-    virtual void TearDown()
+    virtual void TearDown() ORTHANC_OVERRIDE
     {
       image_.reset(NULL);
     }
--- a/UnitTestsSources/ImageTests.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/ImageTests.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -43,9 +43,10 @@
 #include "../Core/Images/PngWriter.h"
 #include "../Core/Images/PamReader.h"
 #include "../Core/Images/PamWriter.h"
+#include "../Core/SystemToolbox.h"
 #include "../Core/Toolbox.h"
 #include "../Core/TemporaryFile.h"
-#include "../OrthancServer/OrthancInitialization.h"  // For the FontRegistry
+#include "../OrthancServer/OrthancConfiguration.h"  // For the FontRegistry
 
 #include <stdint.h>
 
@@ -264,8 +265,12 @@
   Orthanc::Image s(Orthanc::PixelFormat_RGB24, 640, 480, false);
   memset(s.GetBuffer(), 0, s.GetPitch() * s.GetHeight());
 
-  ASSERT_GE(1u, Orthanc::Configuration::GetFontRegistry().GetSize());
-  Orthanc::Configuration::GetFontRegistry().GetFont(0).Draw(s, "Hello world É\n\rComment ça va ?\nq", 50, 60, 255, 0, 0);
+  {
+    Orthanc::OrthancConfiguration::ReaderLock lock;
+    ASSERT_GE(1u, lock.GetConfiguration().GetFontRegistry().GetSize());
+    lock.GetConfiguration().GetFontRegistry().GetFont(0).Draw
+      (s, "Hello world É\n\rComment ça va ?\nq", 50, 60, 255, 0, 0);
+  }
 
   Orthanc::PngWriter w;
   w.WriteToFile("UnitTestsResults/font.png", s);
--- a/UnitTestsSources/LuaTests.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/LuaTests.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -293,7 +293,13 @@
   // OpenSSL/HTTPS support is disabled in curl
   const std::string BASE = "http://www.orthanc-server.com/downloads/third-party/";
 
+#if LUA_VERSION_NUM >= 502
+  // Since Lua >= 5.2.0, the function "loadstring" has been replaced by "load"
+  lua.Execute("JSON = load(HttpGet('" + BASE + "JSON.lua')) ()");
+#else
   lua.Execute("JSON = loadstring(HttpGet('" + BASE + "JSON.lua')) ()");
+#endif
+
   const std::string url(BASE + "Product.json");
 #endif
 
--- a/UnitTestsSources/MemoryCacheTests.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/MemoryCacheTests.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -186,7 +186,7 @@
     {
     }
 
-    virtual ~Integer()
+    virtual ~Integer() ORTHANC_OVERRIDE
     {
       LOG(INFO) << "Removing cache entry for " << value_;
       log_ += boost::lexical_cast<std::string>(value_) + " ";
@@ -198,7 +198,7 @@
   public:
     std::string log_;
 
-    Orthanc::IDynamicObject* Provide(const std::string& s)
+    virtual Orthanc::IDynamicObject* Provide(const std::string& s) ORTHANC_OVERRIDE
     {
       LOG(INFO) << "Providing " << s;
       return new Integer(log_, boost::lexical_cast<int>(s));
@@ -261,9 +261,25 @@
   for (int i = 1; i < 100; i++)
   {
     a.Add(new S("Item " + boost::lexical_cast<std::string>(i)));
+    
     // Continuously protect the two first items
-    try { Orthanc::SharedArchive::Accessor(a, first);  } catch (Orthanc::OrthancException&) {}
-    try { Orthanc::SharedArchive::Accessor(a, second); } catch (Orthanc::OrthancException&) {}
+    {
+      Orthanc::SharedArchive::Accessor accessor(a, first);
+      ASSERT_TRUE(accessor.IsValid());
+      ASSERT_EQ("First item", dynamic_cast<S&>(accessor.GetItem()).GetValue());
+    }
+
+    {
+      Orthanc::SharedArchive::Accessor accessor(a, second);
+      ASSERT_TRUE(accessor.IsValid());
+      ASSERT_EQ("Second item", dynamic_cast<S&>(accessor.GetItem()).GetValue());
+    }
+
+    {
+      Orthanc::SharedArchive::Accessor accessor(a, "nope");
+      ASSERT_FALSE(accessor.IsValid());
+      ASSERT_THROW(accessor.GetItem(), Orthanc::OrthancException);
+    }
   }
 
   std::list<std::string> i;
--- a/UnitTestsSources/MultiThreadingTests.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -93,15 +93,15 @@
     {
     }
 
-    virtual void Start()
+    virtual void Start() ORTHANC_OVERRIDE
     {
     }
 
-    virtual void Reset()
+    virtual void Reset() ORTHANC_OVERRIDE
     {
     }
     
-    virtual JobStepResult Step()
+    virtual JobStepResult Step() ORTHANC_OVERRIDE
     {
       if (fails_)
       {
@@ -118,31 +118,38 @@
       }
     }
 
-    virtual void Stop(JobStopReason reason)
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
     {
     }
 
-    virtual float GetProgress()
+    virtual float GetProgress() ORTHANC_OVERRIDE
     {
       return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
     }
 
-    virtual void GetJobType(std::string& type)
+    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
     {
       type = "DummyJob";
     }
 
-    virtual bool Serialize(Json::Value& value)
+    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
     {
       value = Json::objectValue;
       value["Type"] = "DummyJob";
       return true;
     }
 
-    virtual void GetPublicContent(Json::Value& value)
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
     {
       value["hello"] = "world";
     }
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };
 
 
@@ -152,12 +159,12 @@
     bool   trailingStepDone_;
     
   protected:
-    virtual bool HandleInstance(const std::string& instance)
+    virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE
     {
       return (instance != "nope");
     }
 
-    virtual bool HandleTrailingStep()
+    virtual bool HandleTrailingStep() ORTHANC_OVERRIDE
     {
       if (HasTrailingStep())
       {
@@ -201,11 +208,11 @@
       return trailingStepDone_;
     }
     
-    virtual void Stop(JobStopReason reason)
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
     {
     }
 
-    virtual void GetJobType(std::string& s)
+    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
     {
       s = "DummyInstancesJob";
     }
@@ -215,7 +222,7 @@
   class DummyUnserializer : public GenericJobUnserializer
   {
   public:
-    virtual IJob* UnserializeJob(const Json::Value& value)
+    virtual IJob* UnserializeJob(const Json::Value& value) ORTHANC_OVERRIDE
     {
       if (SerializationToolbox::ReadString(value, "Type") == "DummyInstancesJob")
       {
@@ -329,7 +336,7 @@
 
 TEST(JobsRegistry, Priority)
 {
-  JobsRegistry registry;
+  JobsRegistry registry(10);
 
   std::string i1, i2, i3, i4;
   registry.Submit(i1, new DummyJob(), 10);
@@ -408,7 +415,7 @@
 
 TEST(JobsRegistry, Simultaneous)
 {
-  JobsRegistry registry;
+  JobsRegistry registry(10);
 
   std::string i1, i2;
   registry.Submit(i1, new DummyJob(), 20);
@@ -438,7 +445,7 @@
 
 TEST(JobsRegistry, Resubmit)
 {
-  JobsRegistry registry;
+  JobsRegistry registry(10);
 
   std::string id;
   registry.Submit(id, new DummyJob(), 10);
@@ -482,7 +489,7 @@
 
 TEST(JobsRegistry, Retry)
 {
-  JobsRegistry registry;
+  JobsRegistry registry(10);
 
   std::string id;
   registry.Submit(id, new DummyJob(), 10);
@@ -519,7 +526,7 @@
 
 TEST(JobsRegistry, PausePending)
 {
-  JobsRegistry registry;
+  JobsRegistry registry(10);
 
   std::string id;
   registry.Submit(id, new DummyJob(), 10);
@@ -542,7 +549,7 @@
 
 TEST(JobsRegistry, PauseRunning)
 {
-  JobsRegistry registry;
+  JobsRegistry registry(10);
 
   std::string id;
   registry.Submit(id, new DummyJob(), 10);
@@ -580,7 +587,7 @@
 
 TEST(JobsRegistry, PauseRetry)
 {
-  JobsRegistry registry;
+  JobsRegistry registry(10);
 
   std::string id;
   registry.Submit(id, new DummyJob(), 10);
@@ -617,7 +624,7 @@
 
 TEST(JobsRegistry, Cancel)
 {
-  JobsRegistry registry;
+  JobsRegistry registry(10);
 
   std::string id;
   registry.Submit(id, new DummyJob(), 10);
@@ -711,7 +718,7 @@
 
 TEST(JobsEngine, SubmitAndWait)
 {
-  JobsEngine engine;
+  JobsEngine engine(10);
   engine.SetThreadSleep(10);
   engine.SetWorkersCount(3);
   engine.Start();
@@ -731,7 +738,7 @@
 
 TEST(JobsEngine, DISABLED_SequenceOfOperationsJob)
 {
-  JobsEngine engine;
+  JobsEngine engine(10);
   engine.SetThreadSleep(10);
   engine.SetWorkersCount(3);
   engine.Start();
@@ -771,7 +778,7 @@
 
 TEST(JobsEngine, DISABLED_Lua)
 {
-  JobsEngine engine;
+  JobsEngine engine(10);
   engine.SetThreadSleep(10);
   engine.SetWorkersCount(2);
   engine.Start();
@@ -1282,11 +1289,11 @@
     OrthancJobsSerialization()
     {
       db_.Open();
-      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */));
+      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10));
       context_->SetupJobsEngine(true, false);
     }
 
-    virtual ~OrthancJobsSerialization()
+    virtual ~OrthancJobsSerialization() ORTHANC_OVERRIDE
     {
       context_->Stop();
       context_.reset(NULL);
@@ -1475,8 +1482,7 @@
   // ArchiveJob
 
   {
-    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
-    ArchiveJob job(tmp, GetContext(), false, false);
+    ArchiveJob job(GetContext(), false, false);
     ASSERT_FALSE(job.Serialize(s));  // Cannot serialize this
   }
 
@@ -1704,7 +1710,7 @@
   std::string i1, i2;
 
   {
-    JobsRegistry registry;
+    JobsRegistry registry(10);
     registry.Submit(i1, new DummyJob(), 10);
     registry.Submit(i2, new SequenceOfOperationsJob(), 30);
     registry.Serialize(s);
@@ -1712,7 +1718,7 @@
 
   {
     DummyUnserializer unserializer;
-    JobsRegistry registry(unserializer, s);
+    JobsRegistry registry(unserializer, s, 10);
 
     Json::Value t;
     registry.Serialize(t);
--- a/UnitTestsSources/RestApiTests.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/RestApiTests.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -286,7 +286,7 @@
     virtual bool Visit(const RestApiHierarchy::Resource& resource,
                        const UriComponents& uri,
                        const IHttpHandler::Arguments& components,
-                       const UriComponents& trailing)
+                       const UriComponents& trailing) ORTHANC_OVERRIDE
     {
       return resource.Handle(*(RestApiGetCall*) NULL);
     }
@@ -381,7 +381,7 @@
     }
 
     virtual void Handle(const std::string& type,
-                        const std::string& subtype)
+                        const std::string& subtype) ORTHANC_OVERRIDE
     {
       type_ = type;
       subtype_ = subtype;
--- a/UnitTestsSources/SQLiteChromiumTests.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/SQLiteChromiumTests.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -60,16 +60,16 @@
     {
     }
 
-    virtual ~SQLConnectionTest()
+    virtual ~SQLConnectionTest() ORTHANC_OVERRIDE
     {
     }
 
-    virtual void SetUp() 
+    virtual void SetUp() ORTHANC_OVERRIDE
     {
       db_.OpenInMemory();
     }
 
-    virtual void TearDown() 
+    virtual void TearDown() ORTHANC_OVERRIDE
     {
       db_.Close();
     }
@@ -274,7 +274,7 @@
   class SQLTransactionTest : public SQLConnectionTest
   {
   public:
-    virtual void SetUp()
+    virtual void SetUp() ORTHANC_OVERRIDE
     {
       SQLConnectionTest::SetUp();
       ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
--- a/UnitTestsSources/SQLiteTests.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/SQLiteTests.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -142,22 +142,22 @@
       destroyed = false;
     }
 
-    virtual ~MyFunc()
+    virtual ~MyFunc() ORTHANC_OVERRIDE
     {
       destroyed = true;
     }
 
-    virtual const char* GetName() const
+    virtual const char* GetName() const ORTHANC_OVERRIDE
     {
       return "MYFUNC";
     }
 
-    virtual unsigned int GetCardinality() const
+    virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
     {
       return 2;
     }
 
-    virtual void Compute(SQLite::FunctionContext& context)
+    virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
     {
       context.SetIntResult(1000 + context.GetIntValue(0) * context.GetIntValue(1));
     }
@@ -168,17 +168,17 @@
   public:
     std::set<int> deleted_;
 
-    virtual const char* GetName() const
+    virtual const char* GetName() const ORTHANC_OVERRIDE
     {
       return "MYDELETE";
     }
 
-    virtual unsigned int GetCardinality() const
+    virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
     {
       return 1;
     }
 
-    virtual void Compute(SQLite::FunctionContext& context)
+    virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
     {
       deleted_.insert(context.GetIntValue(0));
       context.SetNullResult();
--- a/UnitTestsSources/ServerIndexTests.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/ServerIndexTests.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -70,20 +70,21 @@
     }
 
     virtual void SignalRemainingAncestor(ResourceType type,
-                                         const std::string& publicId) 
+                                         const std::string& publicId)
+      ORTHANC_OVERRIDE
     {
       ancestorId_ = publicId;
       ancestorType_ = type;
     }
 
-    virtual void SignalFileDeleted(const FileInfo& info)
+    virtual void SignalFileDeleted(const FileInfo& info) ORTHANC_OVERRIDE
     {
       const std::string fileUuid = info.GetUuid();
       deletedFiles_.push_back(fileUuid);
       LOG(INFO) << "A file must be removed: " << fileUuid;
     }       
 
-    virtual void SignalChange(const ServerIndexChange& change)
+    virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE
     {
       if (change.GetChangeType() == ChangeType_Deleted)
       {
@@ -108,7 +109,7 @@
     {
     }
 
-    virtual void SetUp() 
+    virtual void SetUp()  ORTHANC_OVERRIDE
     {
       listener_.reset(new TestDatabaseListener);
 
@@ -126,7 +127,7 @@
       index_->Open();
     }
 
-    virtual void TearDown()
+    virtual void TearDown() ORTHANC_OVERRIDE
     {
       index_->Close();
       index_.reset(NULL);
@@ -676,7 +677,7 @@
   FilesystemStorage storage(path);
   DatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage, true /* running unit tests */);
+  ServerContext context(db, storage, true /* running unit tests */, 10);
   context.SetupJobsEngine(true, false);
 
   ServerIndex& index = context.GetIndex();
@@ -776,7 +777,7 @@
   FilesystemStorage storage(path);
   DatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage, true /* running unit tests */);
+  ServerContext context(db, storage, true /* running unit tests */, 10);
   context.SetupJobsEngine(true, false);
   ServerIndex& index = context.GetIndex();
 
@@ -820,6 +821,11 @@
     ids.push_back(hasher.HashStudy());
     ids.push_back(hasher.HashSeries());
     ids.push_back(hasher.HashInstance());
+
+    ASSERT_EQ(hasher.HashPatient(), toStore.GetHasher().HashPatient());
+    ASSERT_EQ(hasher.HashStudy(), toStore.GetHasher().HashStudy());
+    ASSERT_EQ(hasher.HashSeries(), toStore.GetHasher().HashSeries());
+    ASSERT_EQ(hasher.HashInstance(), toStore.GetHasher().HashInstance());
   }
 
   index.ComputeStatistics(tmp);
@@ -859,7 +865,7 @@
     MemoryStorageArea storage;
     DatabaseWrapper db;   // The SQLite DB is in memory
     db.Open();
-    ServerContext context(db, storage, true /* running unit tests */);
+    ServerContext context(db, storage, true /* running unit tests */, 10);
     context.SetupJobsEngine(true, false);
     context.SetCompressionEnabled(true);
 
@@ -884,6 +890,7 @@
       DicomInstanceToStore toStore;
       toStore.SetSummary(instance);
       toStore.SetOrigin(DicomInstanceOrigin::FromPlugins());
+      ASSERT_EQ(id, toStore.GetHasher().HashInstance());
 
       std::string id2;
       ASSERT_EQ(StoreStatus_Success, context.Store(id2, toStore));
--- a/UnitTestsSources/UnitTestsMain.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -301,26 +301,30 @@
 
 TEST(Uri, AutodetectMimeType)
 {
-  ASSERT_EQ("", Toolbox::AutodetectMimeType("../NOTES"));
-  ASSERT_EQ("", Toolbox::AutodetectMimeType(""));
-  ASSERT_EQ("", Toolbox::AutodetectMimeType("/"));
-  ASSERT_EQ("", Toolbox::AutodetectMimeType("a/a"));
-
-  ASSERT_EQ("text/plain", Toolbox::AutodetectMimeType("../NOTES.txt"));
-  ASSERT_EQ("text/plain", Toolbox::AutodetectMimeType("../coucou.xml/NOTES.txt"));
-  ASSERT_EQ("text/xml", Toolbox::AutodetectMimeType("../.xml"));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("../NOTES"));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType(""));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("/"));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("a/a"));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("..\\a\\"));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("..\\a\\a"));
 
-  ASSERT_EQ("application/javascript", Toolbox::AutodetectMimeType("NOTES.js"));
-  ASSERT_EQ("application/json", Toolbox::AutodetectMimeType("NOTES.json"));
-  ASSERT_EQ("application/pdf", Toolbox::AutodetectMimeType("NOTES.pdf"));
-  ASSERT_EQ("text/css", Toolbox::AutodetectMimeType("NOTES.css"));
-  ASSERT_EQ("text/html", Toolbox::AutodetectMimeType("NOTES.html"));
-  ASSERT_EQ("text/plain", Toolbox::AutodetectMimeType("NOTES.txt"));
-  ASSERT_EQ("text/xml", Toolbox::AutodetectMimeType("NOTES.xml"));
-  ASSERT_EQ("image/gif", Toolbox::AutodetectMimeType("NOTES.gif"));
-  ASSERT_EQ("image/jpeg", Toolbox::AutodetectMimeType("NOTES.jpg"));
-  ASSERT_EQ("image/jpeg", Toolbox::AutodetectMimeType("NOTES.jpeg"));
-  ASSERT_EQ("image/png", Toolbox::AutodetectMimeType("NOTES.png"));
+  ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("../NOTES.txt"));
+  ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("../coucou.xml/NOTES.txt"));
+  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("..\\coucou.\\NOTES.xml"));
+  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("../.xml"));
+  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("../.XmL"));
+
+  ASSERT_EQ(MimeType_JavaScript, SystemToolbox::AutodetectMimeType("NOTES.js"));
+  ASSERT_EQ(MimeType_Json, SystemToolbox::AutodetectMimeType("NOTES.json"));
+  ASSERT_EQ(MimeType_Pdf, SystemToolbox::AutodetectMimeType("NOTES.pdf"));
+  ASSERT_EQ(MimeType_Css, SystemToolbox::AutodetectMimeType("NOTES.css"));
+  ASSERT_EQ(MimeType_Html, SystemToolbox::AutodetectMimeType("NOTES.html"));
+  ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("NOTES.txt"));
+  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("NOTES.xml"));
+  ASSERT_EQ(MimeType_Gif, SystemToolbox::AutodetectMimeType("NOTES.gif"));
+  ASSERT_EQ(MimeType_Jpeg, SystemToolbox::AutodetectMimeType("NOTES.jpg"));
+  ASSERT_EQ(MimeType_Jpeg, SystemToolbox::AutodetectMimeType("NOTES.jpeg"));
+  ASSERT_EQ(MimeType_Png, SystemToolbox::AutodetectMimeType("NOTES.png"));
 }
 
 TEST(Toolbox, ComputeMD5)
@@ -440,6 +444,19 @@
   ASSERT_EQ(0x00, static_cast<unsigned char>(utf8[14]));  // Null-terminated string
 }
 
+
+TEST(Toolbox, FixUtf8)
+{
+  // This is a Latin-1 test string: "crane" with a circumflex accent
+  const unsigned char latin1[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 };
+
+  std::string s((char*) &latin1[0], sizeof(latin1) / sizeof(char));
+
+  ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1), Encoding_Latin1));
+  ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8));
+}
+
+
 TEST(Toolbox, UrlDecode)
 {
   std::string s;
@@ -472,14 +489,20 @@
   s[2] = '\0';
   ASSERT_EQ(10u, s.size());
   ASSERT_FALSE(Toolbox::IsAsciiString(s));
+
+  ASSERT_TRUE(Toolbox::IsAsciiString("Hello\nworld"));
+  ASSERT_FALSE(Toolbox::IsAsciiString("Hello\rworld"));
+
+  ASSERT_EQ("Hello\nworld", Toolbox::ConvertToAscii("Hello\nworld"));
+  ASSERT_EQ("Helloworld", Toolbox::ConvertToAscii("Hello\r\tworld"));
 }
 
 
 #if defined(__linux__)
-TEST(OrthancInitialization, AbsoluteDirectory)
+TEST(Toolbox, AbsoluteDirectory)
 {
-  ASSERT_EQ("/tmp/hello", Configuration::InterpretRelativePath("/tmp", "hello"));
-  ASSERT_EQ("/tmp", Configuration::InterpretRelativePath("/tmp", "/tmp"));
+  ASSERT_EQ("/tmp/hello", SystemToolbox::InterpretRelativePath("/tmp", "hello"));
+  ASSERT_EQ("/tmp", SystemToolbox::InterpretRelativePath("/tmp", "/tmp"));
 }
 #endif
 
@@ -686,6 +709,26 @@
   ASSERT_EQ(JobState_Paused, StringToJobState(EnumerationToString(JobState_Paused)));
   ASSERT_EQ(JobState_Retry, StringToJobState(EnumerationToString(JobState_Retry)));
   ASSERT_THROW(StringToJobState("nope"), OrthancException);
+
+  ASSERT_EQ(MimeType_Binary, StringToMimeType(EnumerationToString(MimeType_Binary)));
+  ASSERT_EQ(MimeType_Dicom, StringToMimeType(EnumerationToString(MimeType_Dicom)));
+  ASSERT_EQ(MimeType_Jpeg, StringToMimeType(EnumerationToString(MimeType_Jpeg)));
+  ASSERT_EQ(MimeType_Jpeg2000, StringToMimeType(EnumerationToString(MimeType_Jpeg2000)));
+  ASSERT_EQ(MimeType_Json, StringToMimeType(EnumerationToString(MimeType_Json)));
+  ASSERT_EQ(MimeType_Pdf, StringToMimeType(EnumerationToString(MimeType_Pdf)));
+  ASSERT_EQ(MimeType_Png, StringToMimeType(EnumerationToString(MimeType_Png)));
+  ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml)));
+  ASSERT_EQ(MimeType_Xml, StringToMimeType("application/xml"));
+  ASSERT_EQ(MimeType_Xml, StringToMimeType("text/xml"));
+  ASSERT_EQ(MimeType_PlainText, StringToMimeType(EnumerationToString(MimeType_PlainText)));
+  ASSERT_EQ(MimeType_Pam, StringToMimeType(EnumerationToString(MimeType_Pam)));
+  ASSERT_EQ(MimeType_Html, StringToMimeType(EnumerationToString(MimeType_Html)));
+  ASSERT_EQ(MimeType_Gzip, StringToMimeType(EnumerationToString(MimeType_Gzip)));
+  ASSERT_EQ(MimeType_JavaScript, StringToMimeType(EnumerationToString(MimeType_JavaScript)));
+  ASSERT_EQ(MimeType_Gif, StringToMimeType(EnumerationToString(MimeType_Gif)));
+  ASSERT_EQ(MimeType_WebAssembly, StringToMimeType(EnumerationToString(MimeType_WebAssembly)));
+  ASSERT_EQ(MimeType_Css, StringToMimeType(EnumerationToString(MimeType_Css)));
+  ASSERT_THROW(StringToMimeType("nope"), OrthancException);
 }
 
 
@@ -1119,6 +1162,34 @@
 }
 
 
+TEST(Toolbox, SubstituteVariables)
+{
+  std::map<std::string, std::string> env;
+  env["NOPE"] = "nope";
+  env["WORLD"] = "world";
+
+  ASSERT_EQ("Hello world\r\nWorld \r\nDone world\r\n",
+            Toolbox::SubstituteVariables(
+              "Hello ${WORLD}\r\nWorld ${HELLO}\r\nDone ${WORLD}\r\n",
+              env));
+
+  ASSERT_EQ("world A a B world C 'c' D {\"a\":\"b\"} E ",
+            Toolbox::SubstituteVariables(
+              "${WORLD} A ${WORLD2:-a} B ${WORLD:-b} C ${WORLD2:-\"'c'\"} D ${WORLD2:-'{\"a\":\"b\"}'} E ${WORLD2:-}",
+              env));
+  
+  SystemToolbox::GetEnvironmentVariables(env);
+  ASSERT_TRUE(env.find("NOPE") == env.end());
+
+  // The "PATH" environment variable should always be available on
+  // machines running the unit tests
+  ASSERT_TRUE(env.find("PATH") != env.end());
+
+  ASSERT_EQ("A" + env["PATH"] + "B",
+            Toolbox::SubstituteVariables("A${PATH}B", env));
+}
+
+
 int main(int argc, char **argv)
 {
   Logging::Initialize();
--- a/UnitTestsSources/VersionsTests.cpp	Thu Oct 18 10:48:11 2018 +0200
+++ b/UnitTestsSources/VersionsTests.cpp	Thu Dec 06 15:58:08 2018 +0100
@@ -104,7 +104,7 @@
 
 TEST(Versions, BoostStatic)
 {
-  ASSERT_STREQ("1_67", BOOST_LIB_VERSION);
+  ASSERT_STREQ("1_68", BOOST_LIB_VERSION);
 }
 
 TEST(Versions, CurlStatic)
@@ -141,7 +141,7 @@
 
 TEST(Version, LuaStatic)
 {
-  ASSERT_STREQ("Lua 5.1.5", LUA_RELEASE);
+  ASSERT_STREQ("Lua 5.3.5", LUA_RELEASE);
 }
 
 TEST(Version, LibIconvStatic)