changeset 1364:111e23bb4904 query-retrieve

integration mainline->query-retrieve
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 21 May 2015 16:58:30 +0200
parents f894be6e7cc1 (current diff) feaf2840917c (diff)
children 38ce915cb455
files Core/DicomFormat/DicomMap.cpp Core/DicomFormat/DicomMap.h Core/DicomFormat/DicomTag.h Core/FileStorage/FileStorage.cpp Core/FileStorage/FileStorage.h OrthancServer/DicomProtocol/DicomUserConnection.cpp OrthancServer/OrthancFindRequestHandler.cpp OrthancServer/ServerIndex.cpp OrthancServer/ServerIndex.h OrthancServer/main.cpp Resources/Patches/mongoose-patch.diff UnitTestsSources/DicomMap.cpp UnitTestsSources/FileStorage.cpp UnitTestsSources/FromDcmtk.cpp UnitTestsSources/JpegLossless.cpp UnitTestsSources/Lua.cpp UnitTestsSources/MemoryCache.cpp UnitTestsSources/MultiThreading.cpp UnitTestsSources/Png.cpp UnitTestsSources/RestApi.cpp UnitTestsSources/SQLite.cpp UnitTestsSources/SQLiteChromium.cpp UnitTestsSources/Versions.cpp UnitTestsSources/Zip.cpp
diffstat 383 files changed, 32178 insertions(+), 7198 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.travis.yml	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,68 @@
+language: cpp
+
+env:
+  - TRAVIS_MINGW=OFF
+  - TRAVIS_MINGW=ON
+
+compiler:
+  - gcc
+  - clang
+
+os:
+  - osx
+  - linux
+
+osx_image: xcode61
+
+matrix:
+  exclude:
+    # This excludes OSX builds from the build matrix for gcc
+    - os: osx
+      compiler: gcc
+
+    # Do not compile for OS X or clang when MinGW is enabled
+    - os: osx
+      env: TRAVIS_MINGW=ON
+    - compiler: clang
+      env: TRAVIS_MINGW=ON
+
+before_install:
+  - if [ $TRAVIS_OS_NAME == linux ]; then sudo apt-get update -qq && sudo apt-get install
+    -qq build-essential unzip cmake mercurial uuid-dev libcurl4-openssl-dev liblua5.1-0-dev
+    libgtest-dev libpng-dev libsqlite3-dev libssl-dev zlib1g-dev libdcmtk2-dev libwrap0-dev
+    libcharls-dev; fi
+  - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then sudo apt-get install mingw32; fi
+
+
+before_script:
+  - mkdir Build
+  - cd Build
+  - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == OFF ]; then cmake
+    -DCMAKE_BUILD_TYPE=Debug "-DDCMTK_LIBRARIES=CharLS;dcmjpls;wrap;oflog"
+    -DALLOW_DOWNLOADS=ON -DUSE_SYSTEM_BOOST=OFF -DUSE_SYSTEM_MONGOOSE=OFF -DUSE_SYSTEM_JSONCPP=OFF
+    -DUSE_SYSTEM_GOOGLE_LOG=OFF -DUSE_SYSTEM_PUGIXML=OFF -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON
+    ..; fi
+  - if [ $TRAVIS_OS_NAME == linux -a $TRAVIS_MINGW == ON ]; then cmake
+    -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON
+    -DCMAKE_TOOLCHAIN_FILE=Resources/MinGWToolchain.cmake
+    ..; fi
+  - if [ $TRAVIS_OS_NAME == osx ]; then cmake 
+    -DCMAKE_BUILD_TYPE=Debug -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON
+    ..; fi
+
+script: make && if [ $TRAVIS_MINGW == OFF ]; then ./UnitTests; fi
+
+#script: cp ../README Orthanc
+#deploy:
+#  provider: releases
+#  api_key:
+#    secure: WU+niKLAKMoJHST5EK23BayK4qXSrXELKlJYc8wRjMO4ay1KSgvzlY2UGKeW1EPClBfZZ0Uh5VKF8l34exsfirFuwCX2qceozduZproUszZ4Z88X8wt8Ctu8tBuuKLZYFc9iNH4zw+QZyRuPyXK9iWpS0L9O20pqy5upTsagM3o=
+#  file_glob: true
+#  file:
+#    - 'Build/Orthanc'
+#    - 'Build/UnitTests'
+#    - 'BuildMinGW32/Orthanc.exe'
+#    - 'BuildMinGW32/UnitTests.exe'
+#  skip_cleanup: true
+#  on:
+#    all_branches: true
--- a/AUTHORS	Wed Jun 25 15:34:40 2014 +0200
+++ b/AUTHORS	Thu May 21 16:58:30 2015 +0200
@@ -6,9 +6,11 @@
 ------------------
 
 * Sebastien Jodogne <s.jodogne@gmail.com>
-  Department of Medical Physics, CHU of Liege, Belgium
+  Department of Medical Physics
+  University Hospital of Liege
+  Belgium
 
-  Overall design and main developper.
+  Overall design and lead developer.
 
 
 Client library
--- a/CMakeLists.txt	Wed Jun 25 15:34:40 2014 +0200
+++ b/CMakeLists.txt	Thu May 21 16:58:30 2015 +0200
@@ -34,22 +34,33 @@
 SET(USE_SYSTEM_CURL ON CACHE BOOL "Use the system version of LibCurl")
 SET(USE_SYSTEM_OPENSSL ON CACHE BOOL "Use the system version of OpenSSL")
 SET(USE_SYSTEM_ZLIB ON CACHE BOOL "Use the system version of ZLib")
+SET(USE_SYSTEM_PUGIXML ON CACHE BOOL "Use the system version of Pugixml)")
+
+# Experimental options
+SET(USE_PUGIXML ON CACHE BOOL "Use the Pugixml parser (turn off only for debug)")
 
 # Distribution-specific settings
 SET(USE_GTEST_DEBIAN_SOURCE_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
+SET(SYSTEM_MONGOOSE_USE_CALLBACKS ON CACHE BOOL "The system version of Mongoose uses callbacks (version >= 3.7)")
+SET(USE_BOOST_ICONV ON CACHE BOOL "Use iconv instead of wconv (Windows only)")
+
 mark_as_advanced(USE_GTEST_DEBIAN_SOURCE_PACKAGE)
+mark_as_advanced(SYSTEM_MONGOOSE_USE_CALLBACKS)
+mark_as_advanced(USE_BOOST_ICONV)
+
+# Path to the root folder of the Orthanc distribution
+set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR})
 
 # Some basic inclusions
 include(CheckIncludeFiles)
 include(CheckIncludeFileCXX)
 include(CheckLibraryExists)
+include(FindPythonInterp)
 include(${CMAKE_SOURCE_DIR}/Resources/CMake/AutoGeneratedCode.cmake)
 include(${CMAKE_SOURCE_DIR}/Resources/CMake/DownloadPackage.cmake)
 include(${CMAKE_SOURCE_DIR}/Resources/CMake/Compiler.cmake)
 include(${CMAKE_SOURCE_DIR}/Resources/CMake/VisualStudioPrecompiledHeaders.cmake)
 
-set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR})
-
 
 
 
@@ -72,7 +83,7 @@
   Core/DicomFormat/DicomIntegerPixelAccessor.cpp
   Core/DicomFormat/DicomInstanceHasher.cpp
   Core/Enumerations.cpp
-  Core/FileStorage/FileStorage.cpp
+  Core/FileStorage/FilesystemStorage.cpp
   Core/FileStorage/StorageAccessor.cpp
   Core/FileStorage/CompressedFileStorageAccessor.cpp
   Core/FileStorage/FileStorageAccessor.cpp
@@ -84,6 +95,9 @@
   Core/HttpServer/MongooseServer.cpp
   Core/HttpServer/HttpFileSender.cpp
   Core/HttpServer/FilesystemHttpSender.cpp
+  Core/RestApi/RestApiCall.cpp
+  Core/RestApi/RestApiGetCall.cpp
+  Core/RestApi/RestApiHierarchy.cpp
   Core/RestApi/RestApiPath.cpp
   Core/RestApi/RestApiOutput.cpp
   Core/RestApi/RestApi.cpp
@@ -91,6 +105,7 @@
   Core/MultiThreading/BagOfRunnablesBySteps.cpp
   Core/MultiThreading/Mutex.cpp
   Core/MultiThreading/ReaderWriterLock.cpp
+  Core/MultiThreading/Semaphore.cpp
   Core/MultiThreading/SharedMessageQueue.cpp
   Core/MultiThreading/ThreadedCommandProcessor.cpp
   Core/ImageFormats/ImageAccessor.cpp
@@ -114,6 +129,11 @@
   OrthancCppClient/Series.cpp
   OrthancCppClient/Instance.cpp
   OrthancCppClient/Patient.cpp
+
+  Plugins/Engine/SharedLibrary.cpp
+  Plugins/Engine/PluginsManager.cpp
+  Plugins/Engine/OrthancPlugins.cpp
+  Plugins/Engine/OrthancPluginDatabase.cpp
   )
 
 
@@ -126,6 +146,7 @@
   OrthancServer/DicomModification.cpp
   OrthancServer/FromDcmtkBridge.cpp
   OrthancServer/ParsedDicomFile.cpp
+  OrthancServer/DicomDirWriter.cpp
   OrthancServer/Internals/CommandDispatcher.cpp
   OrthancServer/Internals/FindScp.cpp
   OrthancServer/Internals/MoveScp.cpp
@@ -148,26 +169,51 @@
   OrthancServer/ServerToolbox.cpp
   OrthancServer/OrthancFindRequestHandler.cpp
   OrthancServer/OrthancMoveRequestHandler.cpp
+  OrthancServer/ExportedResource.cpp
+  OrthancServer/ResourceFinder.cpp
+  OrthancServer/DicomFindQuery.cpp
+
+  # From "lua-scripting" branch
+  OrthancServer/DicomInstanceToStore.cpp
+  OrthancServer/Scheduler/DeleteInstanceCommand.cpp
+  OrthancServer/Scheduler/ModifyInstanceCommand.cpp
+  OrthancServer/Scheduler/ServerCommandInstance.cpp
+  OrthancServer/Scheduler/ServerJob.cpp
+  OrthancServer/Scheduler/ServerScheduler.cpp
+  OrthancServer/Scheduler/StorePeerCommand.cpp
+  OrthancServer/Scheduler/StoreScuCommand.cpp
+  OrthancServer/Scheduler/CallSystemCommand.cpp
   )
 
 
 set(ORTHANC_UNIT_TESTS_SOURCES
-  UnitTestsSources/DicomMap.cpp
-  UnitTestsSources/FileStorage.cpp
-  UnitTestsSources/FromDcmtk.cpp
-  UnitTestsSources/MemoryCache.cpp
-  UnitTestsSources/Png.cpp
-  UnitTestsSources/RestApi.cpp
-  UnitTestsSources/SQLite.cpp
-  UnitTestsSources/SQLiteChromium.cpp
+  UnitTestsSources/DicomMapTests.cpp
+  UnitTestsSources/FileStorageTests.cpp
+  UnitTestsSources/FromDcmtkTests.cpp
+  UnitTestsSources/MemoryCacheTests.cpp
+  UnitTestsSources/PngTests.cpp
+  UnitTestsSources/RestApiTests.cpp
+  UnitTestsSources/SQLiteTests.cpp
+  UnitTestsSources/SQLiteChromiumTests.cpp
   UnitTestsSources/ServerIndexTests.cpp
-  UnitTestsSources/Versions.cpp
-  UnitTestsSources/Zip.cpp
-  UnitTestsSources/Lua.cpp
-  UnitTestsSources/MultiThreading.cpp
+  UnitTestsSources/VersionsTests.cpp
+  UnitTestsSources/ZipTests.cpp
+  UnitTestsSources/LuaTests.cpp
+  UnitTestsSources/MultiThreadingTests.cpp
   UnitTestsSources/UnitTestsMain.cpp
   UnitTestsSources/ImageProcessingTests.cpp
-  UnitTestsSources/JpegLossless.cpp
+  UnitTestsSources/JpegLosslessTests.cpp
+  UnitTestsSources/PluginsTests.cpp
+  )
+
+
+set(ORTHANC_EMBEDDED_FILES
+  PREPARE_DATABASE            ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql
+  UPGRADE_DATABASE_3_TO_4     ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql
+  UPGRADE_DATABASE_4_TO_5     ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade4To5.sql
+  CONFIGURATION_SAMPLE        ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json
+  DICOM_CONFORMANCE_STATEMENT ${CMAKE_CURRENT_SOURCE_DIR}/Resources/DicomConformanceStatement.txt
+  LUA_TOOLBOX                 ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua
   )
 
 
@@ -198,6 +244,7 @@
 include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibPngConfiguration.cmake)
 include(${CMAKE_SOURCE_DIR}/Resources/CMake/LuaConfiguration.cmake)
 include(${CMAKE_SOURCE_DIR}/Resources/CMake/LibCurlConfiguration.cmake)
+include(${CMAKE_SOURCE_DIR}/Resources/CMake/PugixmlConfiguration.cmake)
 
 
 if (${ENABLE_SSL})
@@ -227,19 +274,11 @@
 ## Autogeneration of files
 #####################################################################
 
-# Prepare the embedded files
-set(EMBEDDED_FILES
-  PREPARE_DATABASE ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/PrepareDatabase.sql
-  UPGRADE_DATABASE_3_TO_4 ${CMAKE_CURRENT_SOURCE_DIR}/OrthancServer/Upgrade3To4.sql
-  CONFIGURATION_SAMPLE ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Configuration.json
-  LUA_TOOLBOX ${CMAKE_CURRENT_SOURCE_DIR}/Resources/Toolbox.lua
-  )
-
 if (${STANDALONE_BUILD})
   # We embed all the resources in the binaries for standalone builds
   add_definitions(-DORTHANC_STANDALONE=1)
   EmbedResources(
-    ${EMBEDDED_FILES}
+    ${ORTHANC_EMBEDDED_FILES}
     ORTHANC_EXPLORER ${CMAKE_CURRENT_SOURCE_DIR}/OrthancExplorer
     ${DCMTK_DICTIONARIES}
     )
@@ -249,7 +288,7 @@
     -DORTHANC_PATH=\"${CMAKE_SOURCE_DIR}\"
     )
   EmbedResources(
-    ${EMBEDDED_FILES}
+    ${ORTHANC_EMBEDDED_FILES}
     )
 endif()
 
@@ -309,7 +348,7 @@
   OrthancServer/main.cpp
   )
 
-target_link_libraries(Orthanc ServerLibrary CoreLibrary ${STATIC_LUA} ${STATIC_GOOGLE_LOG})
+target_link_libraries(Orthanc ServerLibrary CoreLibrary ${STATIC_LUA} ${STATIC_GOOGLE_LOG} ${DCMTK_LIBRARIES})
 
 if (${OPENSSL_SOURCES_LENGTH} GREATER 0)
   target_link_libraries(Orthanc OpenSSL)
@@ -338,7 +377,7 @@
   ${GTEST_SOURCES}
   ${ORTHANC_UNIT_TESTS_SOURCES}
   )
-target_link_libraries(UnitTests ServerLibrary CoreLibrary ${STATIC_LUA} ${STATIC_GOOGLE_LOG})
+target_link_libraries(UnitTests ServerLibrary CoreLibrary ${STATIC_LUA} ${STATIC_GOOGLE_LOG} ${DCMTK_LIBRARIES})
 
 if (${OPENSSL_SOURCES_LENGTH} GREATER 0)
   target_link_libraries(UnitTests OpenSSL)
@@ -395,7 +434,8 @@
     )
 
   if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
+      ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
     set_target_properties(OrthancClient
       PROPERTIES LINK_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined -Wl,--as-needed -Wl,--version-script=${ORTHANC_ROOT}/OrthancCppClient/SharedLibrary/Laaw/VersionScript.map"
       )
@@ -442,7 +482,11 @@
     )
 
   install(
-    FILES ${ORTHANC_ROOT}/OrthancCppClient/SharedLibrary/AUTOGENERATED/OrthancCppClient.h 
+    FILES
+    ${ORTHANC_ROOT}/OrthancCppClient/SharedLibrary/AUTOGENERATED/OrthancCppClient.h 
+    ${ORTHANC_ROOT}/Plugins/Include/OrthancCPlugin.h 
+    ${ORTHANC_ROOT}/Plugins/Include/OrthancCDatabasePlugin.h 
+    ${ORTHANC_ROOT}/Plugins/Include/OrthancCppDatabasePlugin.h 
     DESTINATION include/orthanc
     )
 endif()
@@ -461,10 +505,26 @@
     ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen
     @ONLY)
 
+  configure_file(
+    ${CMAKE_SOURCE_DIR}/Resources/OrthancPlugin.doxygen
+    ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen
+    @ONLY)
+
   add_custom_target(doc
     ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Orthanc.doxygen
+    COMMENT "Generating internal documentation with Doxygen" VERBATIM
+    )
+
+  add_custom_command(TARGET Orthanc
+    POST_BUILD
+    COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/OrthancPlugin.doxygen
     WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
-    COMMENT "Generating internal documentation with Doxygen" VERBATIM
+    COMMENT "Generating plugin documentation with Doxygen" VERBATIM
+    )
+
+  install(
+    DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/OrthancPluginDocumentation/doc/
+    DESTINATION share/doc/orthanc/OrthancPlugin
     )
 
   if (BUILD_CLIENT_LIBRARY)
--- a/Core/Cache/ICachePageProvider.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Cache/ICachePageProvider.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Cache/LeastRecentlyUsedIndex.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Cache/LeastRecentlyUsedIndex.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Cache/MemoryCache.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Cache/MemoryCache.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Cache/MemoryCache.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Cache/MemoryCache.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/ChunkedBuffer.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ChunkedBuffer.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/ChunkedBuffer.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ChunkedBuffer.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/BufferCompressor.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Compression/BufferCompressor.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/BufferCompressor.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Compression/BufferCompressor.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/HierarchicalZipWriter.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Compression/HierarchicalZipWriter.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -53,7 +53,7 @@
       if (c == '^')
         c = ' ';
 
-      if (c < 128 && 
+      if (c <= 127 && 
           c >= 0)
       {
         if (isspace(c)) 
--- a/Core/Compression/HierarchicalZipWriter.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Compression/HierarchicalZipWriter.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -63,8 +63,6 @@
   
       Stack stack_;
 
-      std::string GetCurrentDirectoryPath() const;
-
       std::string EnsureUniqueFilename(const char* filename);
 
     public:
@@ -83,6 +81,8 @@
 
       void CloseDirectory();
 
+      std::string GetCurrentDirectoryPath() const;
+
       static std::string KeepAlphanumeric(const std::string& source);
     };
 
@@ -114,12 +114,27 @@
       return writer_.GetCompressionLevel();
     }
 
+    void SetAppendToExisting(bool append)
+    {
+      writer_.SetAppendToExisting(append);
+    }
+    
+    bool IsAppendToExisting() const
+    {
+      return writer_.IsAppendToExisting();
+    }
+    
     void OpenFile(const char* name);
 
     void OpenDirectory(const char* name);
 
     void CloseDirectory();
 
+    std::string GetCurrentDirectoryPath() const
+    {
+      return indexer_.GetCurrentDirectoryPath();
+    }
+
     void Write(const char* data, size_t length)
     {
       writer_.Write(data, length);
--- a/Core/Compression/ZipWriter.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Compression/ZipWriter.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -38,10 +38,11 @@
 
 #include "ZipWriter.h"
 
-#include "../../Resources/ThirdParty/minizip/zip.h"
+#include <limits>
+#include <boost/filesystem.hpp>
 #include <boost/date_time/posix_time/posix_time.hpp>
-#include <limits>
 
+#include "../../Resources/ThirdParty/minizip/zip.h"
 #include "../OrthancException.h"
 
 
@@ -77,15 +78,19 @@
   struct ZipWriter::PImpl
   {
     zipFile file_;
+
+    PImpl() : file_(NULL)
+    {
+    }
   };
 
-  ZipWriter::ZipWriter() : pimpl_(new PImpl)
+  ZipWriter::ZipWriter() :
+    pimpl_(new PImpl),
+    isZip64_(false),
+    hasFileInZip_(false),
+    append_(false),
+    compressionLevel_(6)
   {
-    compressionLevel_ = 6;
-    hasFileInZip_ = false;
-    isZip64_ = false;
-
-    pimpl_->file_ = NULL;
   }
 
   ZipWriter::~ZipWriter()
@@ -122,13 +127,20 @@
 
     hasFileInZip_ = false;
 
+    int mode = APPEND_STATUS_CREATE;
+    if (append_ && 
+        boost::filesystem::exists(path_))
+    {
+      mode = APPEND_STATUS_ADDINZIP;
+    }
+
     if (isZip64_)
     {
-      pimpl_->file_ = zipOpen64(path_.c_str(), APPEND_STATUS_CREATE);
+      pimpl_->file_ = zipOpen64(path_.c_str(), mode);
     }
     else
     {
-      pimpl_->file_ = zipOpen(path_.c_str(), APPEND_STATUS_CREATE);
+      pimpl_->file_ = zipOpen(path_.c_str(), mode);
     }
 
     if (!pimpl_->file_)
@@ -230,4 +242,13 @@
       length -= bytes;
     }
   }
+
+
+  void ZipWriter::SetAppendToExisting(bool append)
+  {
+    Close();
+    append_ = append;
+  }
+    
+
 }
--- a/Core/Compression/ZipWriter.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Compression/ZipWriter.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -50,6 +50,7 @@
 
     bool isZip64_;
     bool hasFileInZip_;
+    bool append_;
     uint8_t compressionLevel_;
     std::string path_;
 
@@ -71,6 +72,13 @@
     {
       return compressionLevel_;
     }
+
+    void SetAppendToExisting(bool append);
+    
+    bool IsAppendToExisting() const
+    {
+      return append_;
+    }
     
     void Open();
 
--- a/Core/Compression/ZlibCompressor.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Compression/ZlibCompressor.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Compression/ZlibCompressor.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Compression/ZlibCompressor.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomArray.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomArray.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomArray.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomArray.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomElement.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomElement.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomImageInformation.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomImageInformation.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -39,6 +39,7 @@
 #include "DicomImageInformation.h"
 
 #include "../OrthancException.h"
+#include "../Toolbox.h"
 #include <boost/lexical_cast.hpp>
 #include <limits>
 #include <cassert>
@@ -53,6 +54,66 @@
 
     try
     {
+      std::string p = values.GetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION).AsString();
+      Toolbox::ToUpperCase(p);
+
+      if (p == "RGB")
+      {
+        photometric_ = PhotometricInterpretation_RGB;
+      }
+      else if (p == "MONOCHROME1")
+      {
+        photometric_ = PhotometricInterpretation_Monochrome1;
+      }
+      else if (p == "MONOCHROME2")
+      {
+        photometric_ = PhotometricInterpretation_Monochrome2;
+      }
+      else if (p == "PALETTE COLOR")
+      {
+        photometric_ = PhotometricInterpretation_Palette;
+      }
+      else if (p == "HSV")
+      {
+        photometric_ = PhotometricInterpretation_HSV;
+      }
+      else if (p == "ARGB")
+      {
+        photometric_ = PhotometricInterpretation_ARGB;
+      }
+      else if (p == "CMYK")
+      {
+        photometric_ = PhotometricInterpretation_CMYK;
+      }
+      else if (p == "YBR_FULL")
+      {
+        photometric_ = PhotometricInterpretation_YBRFull;
+      }
+      else if (p == "YBR_FULL_422")
+      {
+        photometric_ = PhotometricInterpretation_YBRFull422;
+      }
+      else if (p == "YBR_PARTIAL_420")
+      {
+        photometric_ = PhotometricInterpretation_YBRPartial420;
+      }
+      else if (p == "YBR_PARTIAL_422")
+      {
+        photometric_ = PhotometricInterpretation_YBRPartial422;
+      }
+      else if (p == "YBR_ICT")
+      {
+        photometric_ = PhotometricInterpretation_YBR_ICT;
+      }
+      else if (p == "YBR_RCT")
+      {
+        photometric_ = PhotometricInterpretation_YBR_RCT;
+      }
+      else
+      {
+        photometric_ = PhotometricInterpretation_Unknown;
+      }
+
       width_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_COLUMNS).AsString());
       height_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_ROWS).AsString());
       bitsAllocated_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_BITS_ALLOCATED).AsString());
@@ -159,30 +220,37 @@
 
   bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format) const
   {
-    if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned())
+    if (photometric_ == PhotometricInterpretation_Monochrome1 ||
+        photometric_ == PhotometricInterpretation_Monochrome2)
     {
-      format = PixelFormat_Grayscale8;
-      return true;
+      if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_Grayscale8;
+        return true;
+      }
+      
+      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_Grayscale16;
+        return true;
+      }
+
+      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && IsSigned())
+      {
+        format = PixelFormat_SignedGrayscale16;
+        return true;
+      }
     }
 
-    if (GetBitsStored() == 8 && GetChannelCount() == 3 && !IsSigned())
+    if (GetBitsStored() == 8 && 
+        GetChannelCount() == 3 && 
+        !IsSigned() &&
+        photometric_ == PhotometricInterpretation_RGB)
     {
       format = PixelFormat_RGB24;
       return true;
     }
 
-    if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && !IsSigned())
-    {
-      format = PixelFormat_Grayscale16;
-      return true;
-    }
-
-    if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && IsSigned())
-    {
-      format = PixelFormat_SignedGrayscale16;
-      return true;
-    }
-
     return false;
   }
 }
--- a/Core/DicomFormat/DicomImageInformation.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomImageInformation.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -54,6 +54,8 @@
     unsigned int bitsStored_;
     unsigned int highBit_;
 
+    PhotometricInterpretation  photometric_;
+
   public:
     DicomImageInformation(const DicomMap& values);
 
@@ -112,6 +114,11 @@
       return highBit_ + 1 - bitsStored_;
     }
 
+    PhotometricInterpretation GetPhotometricInterpretation() const
+    {
+      return photometric_;
+    }
+
     bool ExtractPixelFormat(PixelFormat& format) const;
   };
 }
--- a/Core/DicomFormat/DicomInstanceHasher.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomInstanceHasher.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -48,8 +48,7 @@
     seriesUid_ = seriesUid;
     instanceUid_ = instanceUid;
 
-    if (patientId_.size() == 0 ||
-        studyUid_.size() == 0 ||
+    if (studyUid_.size() == 0 ||
         seriesUid_.size() == 0 ||
         instanceUid_.size() == 0)
     {
@@ -59,7 +58,9 @@
 
   DicomInstanceHasher::DicomInstanceHasher(const DicomMap& instance)
   {
-    Setup(instance.GetValue(DICOM_TAG_PATIENT_ID).AsString(),
+    const DicomValue* patientId = instance.TestAndGetValue(DICOM_TAG_PATIENT_ID);
+
+    Setup(patientId == NULL ? "" : patientId->AsString(),
           instance.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString(),
           instance.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString(),
           instance.GetValue(DICOM_TAG_SOP_INSTANCE_UID).AsString());
--- a/Core/DicomFormat/DicomInstanceHasher.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomInstanceHasher.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomIntegerPixelAccessor.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomIntegerPixelAccessor.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -80,5 +80,10 @@
     {
       return pixelData_;
     }
+
+    size_t GetSize() const
+    {
+      return size_;
+    }
   };
 }
--- a/Core/DicomFormat/DicomMap.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomMap.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,6 +36,7 @@
 #include <stdio.h>
 #include <memory>
 #include "DicomString.h"
+#include "DicomArray.h"
 #include "../OrthancException.h"
 
 
@@ -190,6 +191,17 @@
   }
 
 
+  void DicomMap::Assign(const DicomMap& other)
+  {
+    Clear();
+
+    for (Map::const_iterator it = other.map_.begin(); it != other.map_.end(); ++it)
+    {
+      map_.insert(std::make_pair(it->first, it->second->Clone()));
+    }
+  }
+
+
   const DicomValue& DicomMap::GetValue(const DicomTag& tag) const
   {
     const DicomValue* value = TestAndGetValue(tag);
@@ -389,30 +401,21 @@
   }
 
 
-  void DicomMap::ExtractMainDicomTagsForLevel(DicomMap& result,
-                                              ResourceType level) const
+  void DicomMap::Print(FILE* fp) const
   {
-    switch (level)
-    {
-      case ResourceType_Patient:
-        ExtractPatientInformation(result);
-        break;
+    DicomArray a(*this);
+    a.Print(fp);
+  }
+
 
-      case ResourceType_Study:
-        ExtractStudyInformation(result);
-        break;
+  void DicomMap::GetTags(std::set<DicomTag>& tags) const
+  {
+    tags.clear();
 
-      case ResourceType_Series:
-        ExtractSeriesInformation(result);
-        break;
-
-      case ResourceType_Instance:
-        ExtractInstanceInformation(result);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    for (Map::const_iterator it = map_.begin();
+         it != map_.end(); ++it)
+    {
+      tags.insert(it->first);
     }
   }
-
 }
--- a/Core/DicomFormat/DicomMap.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomMap.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -78,8 +78,15 @@
       Clear();
     }
 
+    size_t GetSize() const
+    {
+      return map_.size();
+    }
+    
     DicomMap* Clone() const;
 
+    void Assign(const DicomMap& other);
+
     void Clear();
 
     void SetValue(uint16_t group, 
@@ -125,11 +132,13 @@
 
     const DicomValue& GetValue(const DicomTag& tag) const;
 
+    // DO NOT delete the returned value!
     const DicomValue* TestAndGetValue(uint16_t group, uint16_t element) const
     {
       return TestAndGetValue(DicomTag(group, element));
     }       
 
+    // DO NOT delete the returned value!
     const DicomValue* TestAndGetValue(const DicomTag& tag) const;
 
     void Remove(const DicomTag& tag);
@@ -161,7 +170,8 @@
 
     static void GetMainDicomTags(std::set<DicomTag>& result);
 
-    void ExtractMainDicomTagsForLevel(DicomMap& result,
-                                      ResourceType level) const;
+    void Print(FILE* fp) const;
+
+    void GetTags(std::set<DicomTag>& tags) const;
   };
 }
--- a/Core/DicomFormat/DicomNullValue.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomNullValue.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomString.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomString.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/DicomFormat/DicomTag.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomTag.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -116,4 +116,142 @@
 
     return "";
   }
+
+
+  void DicomTag::GetTagsForModule(std::set<DicomTag>& target,
+                                  DicomModule module)
+  {
+    // REFERENCE: 11_03pu.pdf, DICOM PS 3.3 2011 - Information Object Definitions
+    target.clear();
+
+    switch (module)
+    {
+      case DicomModule_Patient:
+        // This is Table C.7-1 "Patient Module Attributes" (p. 373)
+        target.insert(DicomTag(0x0010, 0x0010));   // Patient's name
+        target.insert(DicomTag(0x0010, 0x0020));   // Patient ID
+        target.insert(DicomTag(0x0010, 0x0030));   // Patient's birth date
+        target.insert(DicomTag(0x0010, 0x0040));   // Patient's sex
+        target.insert(DicomTag(0x0008, 0x1120));   // Referenced patient sequence
+        target.insert(DicomTag(0x0010, 0x0032));   // Patient's birth time
+        target.insert(DicomTag(0x0010, 0x1000));   // Other patient IDs
+        target.insert(DicomTag(0x0010, 0x1002));   // Other patient IDs sequence
+        target.insert(DicomTag(0x0010, 0x1001));   // Other patient names
+        target.insert(DicomTag(0x0010, 0x2160));   // Ethnic group
+        target.insert(DicomTag(0x0010, 0x4000));   // Patient comments
+        target.insert(DicomTag(0x0010, 0x2201));   // Patient species description
+        target.insert(DicomTag(0x0010, 0x2202));   // Patient species code sequence
+        target.insert(DicomTag(0x0010, 0x2292));   // Patient breed description
+        target.insert(DicomTag(0x0010, 0x2293));   // Patient breed code sequence
+        target.insert(DicomTag(0x0010, 0x2294));   // Breed registration sequence
+        target.insert(DicomTag(0x0010, 0x2297));   // Responsible person
+        target.insert(DicomTag(0x0010, 0x2298));   // Responsible person role
+        target.insert(DicomTag(0x0010, 0x2299));   // Responsible organization
+        target.insert(DicomTag(0x0012, 0x0062));   // Patient identity removed
+        target.insert(DicomTag(0x0012, 0x0063));   // De-identification method
+        target.insert(DicomTag(0x0012, 0x0064));   // De-identification method code sequence
+
+        // Table 10-18 ISSUER OF PATIENT ID MACRO (p. 112)
+        target.insert(DicomTag(0x0010, 0x0021));   // Issuer of Patient ID
+        target.insert(DicomTag(0x0010, 0x0024));   // Issuer of Patient ID qualifiers sequence
+        break;
+
+      case DicomModule_Study:
+        // This is Table C.7-3 "General Study Module Attributes" (p. 378)
+        target.insert(DicomTag(0x0020, 0x000d));   // Study instance UID
+        target.insert(DicomTag(0x0008, 0x0020));   // Study date
+        target.insert(DicomTag(0x0008, 0x0030));   // Study time
+        target.insert(DicomTag(0x0008, 0x0090));   // Referring physician's name
+        target.insert(DicomTag(0x0008, 0x0096));   // Referring physician identification sequence
+        target.insert(DicomTag(0x0020, 0x0010));   // Study ID
+        target.insert(DicomTag(0x0008, 0x0050));   // Accession number
+        target.insert(DicomTag(0x0008, 0x0051));   // Issuer of accession number sequence
+        target.insert(DicomTag(0x0008, 0x1030));   // Study description
+        target.insert(DicomTag(0x0008, 0x1048));   // Physician(s) of record
+        target.insert(DicomTag(0x0008, 0x1049));   // Physician(s) of record identification sequence
+        target.insert(DicomTag(0x0008, 0x1060));   // Name of physician(s) reading study
+        target.insert(DicomTag(0x0008, 0x1062));   // Physician(s) reading study identification sequence
+        target.insert(DicomTag(0x0032, 0x1034));   // Requesting service code sequence
+        target.insert(DicomTag(0x0008, 0x1110));   // Referenced study sequence
+        target.insert(DicomTag(0x0008, 0x1032));   // Procedure code sequence
+        target.insert(DicomTag(0x0040, 0x1012));   // Reason for performed procedure code sequence
+        break;
+
+      case DicomModule_Series:
+        // This is Table C.7-5 "General Series Module Attributes" (p. 385)
+        target.insert(DicomTag(0x0008, 0x0060));   // Modality 
+        target.insert(DicomTag(0x0020, 0x000e));   // Series Instance UID 
+        target.insert(DicomTag(0x0020, 0x0011));   // Series Number 
+        target.insert(DicomTag(0x0020, 0x0060));   // Laterality 
+        target.insert(DicomTag(0x0008, 0x0021));   // Series Date 
+        target.insert(DicomTag(0x0008, 0x0031));   // Series Time 
+        target.insert(DicomTag(0x0008, 0x1050));   // Performing Physicians’ Name 
+        target.insert(DicomTag(0x0008, 0x1052));   // Performing Physician Identification Sequence 
+        target.insert(DicomTag(0x0018, 0x1030));   // Protocol Name
+        target.insert(DicomTag(0x0008, 0x103e));   // Series Description 
+        target.insert(DicomTag(0x0008, 0x103f));   // Series Description Code Sequence 
+        target.insert(DicomTag(0x0008, 0x1070));   // Operators' Name 
+        target.insert(DicomTag(0x0008, 0x1072));   // Operator Identification Sequence 
+        target.insert(DicomTag(0x0008, 0x1111));   // Referenced Performed Procedure Step Sequence
+        target.insert(DicomTag(0x0008, 0x1250));   // Related Series Sequence
+        target.insert(DicomTag(0x0018, 0x0015));   // Body Part Examined
+        target.insert(DicomTag(0x0018, 0x5100));   // Patient Position
+        target.insert(DicomTag(0x0028, 0x0108));   // Smallest Pixel Value in Series 
+        target.insert(DicomTag(0x0029, 0x0109));   // Largest Pixel Value in Series 
+        target.insert(DicomTag(0x0040, 0x0275));   // Request Attributes Sequence 
+        target.insert(DicomTag(0x0010, 0x2210));   // Anatomical Orientation Type
+
+        // Table 10-16 PERFORMED PROCEDURE STEP SUMMARY MACRO ATTRIBUTES
+        target.insert(DicomTag(0x0040, 0x0253));   // Performed Procedure Step ID 
+        target.insert(DicomTag(0x0040, 0x0244));   // Performed Procedure Step Start Date 
+        target.insert(DicomTag(0x0040, 0x0245));   // Performed Procedure Step Start Time 
+        target.insert(DicomTag(0x0040, 0x0254));   // Performed Procedure Step Description 
+        target.insert(DicomTag(0x0040, 0x0260));   // Performed Protocol Code Sequence 
+        target.insert(DicomTag(0x0040, 0x0280));   // Comments on the Performed Procedure Step
+        break;
+
+      case DicomModule_Instance:
+        // This is Table C.12-1 "SOP Common Module Attributes" (p. 1207)
+        target.insert(DicomTag(0x0008, 0x0016));   // SOP Class UID
+        target.insert(DicomTag(0x0008, 0x0018));   // SOP Instance UID 
+        target.insert(DicomTag(0x0008, 0x0005));   // Specific Character Set 
+        target.insert(DicomTag(0x0008, 0x0012));   // Instance Creation Date 
+        target.insert(DicomTag(0x0008, 0x0013));   // Instance Creation Time 
+        target.insert(DicomTag(0x0008, 0x0014));   // Instance Creator UID 
+        target.insert(DicomTag(0x0008, 0x001a));   // Related General SOP Class UID 
+        target.insert(DicomTag(0x0008, 0x001b));   // Original Specialized SOP Class UID 
+        target.insert(DicomTag(0x0008, 0x0110));   // Coding Scheme Identification Sequence 
+        target.insert(DicomTag(0x0008, 0x0201));   // Timezone Offset From UTC 
+        target.insert(DicomTag(0x0018, 0xa001));   // Contributing Equipment Sequence
+        target.insert(DicomTag(0x0020, 0x0013));   // Instance Number 
+        target.insert(DicomTag(0x0100, 0x0410));   // SOP Instance Status 
+        target.insert(DicomTag(0x0100, 0x0420));   // SOP Authorization DateTime 
+        target.insert(DicomTag(0x0100, 0x0424));   // SOP Authorization Comment 
+        target.insert(DicomTag(0x0100, 0x0426));   // Authorization Equipment Certification Number
+        target.insert(DicomTag(0x0400, 0x0500));   // Encrypted Attributes Sequence
+        target.insert(DicomTag(0x0400, 0x0561));   // Original Attributes Sequence 
+        target.insert(DicomTag(0x0040, 0xa390));   // HL7 Structured Document Reference Sequence
+        target.insert(DicomTag(0x0028, 0x0303));   // Longitudinal Temporal Information Modified 
+
+        // Table C.12-6 "DIGITAL SIGNATURES MACRO ATTRIBUTES" (p. 1216)
+        target.insert(DicomTag(0x4ffe, 0x0001));   // MAC Parameters sequence
+        target.insert(DicomTag(0xfffa, 0xfffa));   // Digital signatures sequence
+        break;
+
+        // TODO IMAGE MODULE?
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool DicomTag::IsIdentifier() const
+  {
+    return (*this == DICOM_TAG_PATIENT_ID ||
+            *this == DICOM_TAG_STUDY_INSTANCE_UID ||
+            *this == DICOM_TAG_ACCESSION_NUMBER ||
+            *this == DICOM_TAG_SERIES_INSTANCE_UID ||
+            *this == DICOM_TAG_SOP_INSTANCE_UID);
+  }
 }
--- a/Core/DicomFormat/DicomTag.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomTag.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,8 +33,10 @@
 #pragma once
 
 #include <string>
+#include <set>
 #include <stdint.h>
 
+#include "../Enumerations.h"
 
 namespace Orthanc
 {
@@ -81,6 +83,11 @@
     std::string Format() const;
 
     friend std::ostream& operator<< (std::ostream& o, const DicomTag& tag);
+
+    static void GetTagsForModule(std::set<DicomTag>& target,
+                                 DicomModule module);
+
+    bool IsIdentifier() const;
   };
 
   // Aliases for the most useful tags
@@ -104,10 +111,11 @@
 
   static const DicomTag DICOM_TAG_PATIENT_NAME(0x0010, 0x0010);
 
-  // The following is used for "modify" operations
+  // The following is used for "modify/anonymize" operations
   static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016);
   static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID(0x0002, 0x0002);
   static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID(0x0002, 0x0003);
+  static const DicomTag DICOM_TAG_DEIDENTIFICATION_METHOD(0x0012, 0x0063);
 
   // DICOM tags used for fMRI (thanks to Will Ryder)
   static const DicomTag DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS(0x0020, 0x0105);
--- a/Core/DicomFormat/DicomValue.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/DicomFormat/DicomValue.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/EnumerationDictionary.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/EnumerationDictionary.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,6 +34,7 @@
 
 #include "OrthancException.h"
 
+#include "Toolbox.h"
 #include <boost/lexical_cast.hpp>
 #include <string>
 #include <map>
@@ -53,26 +54,23 @@
       StringToEnumeration stringToEnumeration_;
 
     public:
+      void Clear()
+      {
+        enumerationToString_.clear();
+        stringToEnumeration_.clear();
+      }
+
       void Add(Enumeration value, const std::string& str)
       {
         // Check if these values are free
         if (enumerationToString_.find(value) != enumerationToString_.end() ||
-            stringToEnumeration_.find(str) != stringToEnumeration_.end())
+            stringToEnumeration_.find(str) != stringToEnumeration_.end() ||
+            Toolbox::IsInteger(str) /* Prevent the registration of a number */)
         {
           throw OrthancException(ErrorCode_BadRequest);
         }
 
-        // Prevent the registration of a number
-        try
-        {
-          boost::lexical_cast<int>(str);
-          throw OrthancException(ErrorCode_BadRequest);
-        }
-        catch (boost::bad_lexical_cast)
-        {
-          // OK, the string is not a number
-        }
-
+        // OK, the string is free and is not a number
         enumerationToString_[value] = str;
         stringToEnumeration_[str] = value;
         stringToEnumeration_[boost::lexical_cast<std::string>(static_cast<int>(value))] = value;
--- a/Core/Enumerations.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Enumerations.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -261,6 +261,197 @@
   }
 
 
+  const char* EnumerationToString(Encoding encoding)
+  {
+    switch (encoding)
+    {
+      case Encoding_Ascii:
+        return "Ascii";
+
+      case Encoding_Utf8:
+        return "Utf8";
+
+      case Encoding_Latin1:
+        return "Latin1";
+
+      case Encoding_Latin2:
+        return "Latin2";
+
+      case Encoding_Latin3:
+        return "Latin3";
+
+      case Encoding_Latin4:
+        return "Latin4";
+
+      case Encoding_Latin5:
+        return "Latin5";
+
+      case Encoding_Cyrillic:
+        return "Cyrillic";
+
+      case Encoding_Windows1251:
+        return "Windows1251";
+
+      case Encoding_Arabic:
+        return "Arabic";
+
+      case Encoding_Greek:
+        return "Greek";
+
+      case Encoding_Hebrew:
+        return "Hebrew";
+
+      case Encoding_Thai:
+        return "Thai";
+
+      case Encoding_Japanese:
+        return "Japanese";
+
+      case Encoding_Chinese:
+        return "Chinese";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(PhotometricInterpretation photometric)
+  {
+    switch (photometric)
+    {
+      case PhotometricInterpretation_RGB:
+        return "RGB";
+
+      case PhotometricInterpretation_Monochrome1:
+        return "Monochrome1";
+
+      case PhotometricInterpretation_Monochrome2:
+        return "Monochrome2";
+
+      case PhotometricInterpretation_ARGB:
+        return "ARGB";
+
+      case PhotometricInterpretation_CMYK:
+        return "CMYK";
+
+      case PhotometricInterpretation_HSV:
+        return "HSV";
+
+      case PhotometricInterpretation_Palette:
+        return "Palette color";
+
+      case PhotometricInterpretation_YBRFull:
+        return "YBR full";
+
+      case PhotometricInterpretation_YBRFull422:
+        return "YBR full 422";
+
+      case PhotometricInterpretation_YBRPartial420:
+        return "YBR partial 420"; 
+
+      case PhotometricInterpretation_YBRPartial422:
+        return "YBR partial 422"; 
+
+      case PhotometricInterpretation_YBR_ICT:
+        return "YBR ICT"; 
+
+      case PhotometricInterpretation_YBR_RCT:
+        return "YBR RCT"; 
+
+      case PhotometricInterpretation_Unknown:
+        return "Unknown";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  Encoding StringToEncoding(const char* encoding)
+  {
+    std::string s(encoding);
+    Toolbox::ToUpperCase(s);
+
+    if (s == "UTF8")
+    {
+      return Encoding_Utf8;
+    }
+
+    if (s == "ASCII")
+    {
+      return Encoding_Ascii;
+    }
+
+    if (s == "LATIN1")
+    {
+      return Encoding_Latin1;
+    }
+
+    if (s == "LATIN2")
+    {
+      return Encoding_Latin2;
+    }
+
+    if (s == "LATIN3")
+    {
+      return Encoding_Latin3;
+    }
+
+    if (s == "LATIN4")
+    {
+      return Encoding_Latin4;
+    }
+
+    if (s == "LATIN5")
+    {
+      return Encoding_Latin5;
+    }
+
+    if (s == "CYRILLIC")
+    {
+      return Encoding_Cyrillic;
+    }
+
+    if (s == "WINDOWS1251")
+    {
+      return Encoding_Windows1251;
+    }
+
+    if (s == "ARABIC")
+    {
+      return Encoding_Arabic;
+    }
+
+    if (s == "GREEK")
+    {
+      return Encoding_Greek;
+    }
+
+    if (s == "HEBREW")
+    {
+      return Encoding_Hebrew;
+    }
+
+    if (s == "THAI")
+    {
+      return Encoding_Thai;
+    }
+
+    if (s == "JAPANESE")
+    {
+      return Encoding_Japanese;
+    }
+
+    if (s == "CHINESE")
+    {
+      return Encoding_Chinese;
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+
+
   ResourceType StringToResourceType(const char* type)
   {
     std::string s(type);
@@ -323,4 +514,115 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
+
+
+  bool GetDicomEncoding(Encoding& encoding,
+                        const char* specificCharacterSet)
+  {
+    std::string s = specificCharacterSet;
+    Toolbox::ToUpperCase(s);
+
+    // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/
+    // https://github.com/dcm4che/dcm4che/blob/master/dcm4che-core/src/main/java/org/dcm4che3/data/SpecificCharacterSet.java
+    if (s == "ISO_IR 6" ||
+        s == "ISO_IR 192" ||
+        s == "ISO 2022 IR 6")
+    {
+      encoding = Encoding_Utf8;
+    }
+    else if (s == "ISO_IR 100" ||
+             s == "ISO 2022 IR 100")
+    {
+      encoding = Encoding_Latin1;
+    }
+    else if (s == "ISO_IR 101" ||
+             s == "ISO 2022 IR 101")
+    {
+      encoding = Encoding_Latin2;
+    }
+    else if (s == "ISO_IR 109" ||
+             s == "ISO 2022 IR 109")
+    {
+      encoding = Encoding_Latin3;
+    }
+    else if (s == "ISO_IR 110" ||
+             s == "ISO 2022 IR 110")
+    {
+      encoding = Encoding_Latin4;
+    }
+    else if (s == "ISO_IR 148" ||
+             s == "ISO 2022 IR 148")
+    {
+      encoding = Encoding_Latin5;
+    }
+    else if (s == "ISO_IR 144" ||
+             s == "ISO 2022 IR 144")
+    {
+      encoding = Encoding_Cyrillic;
+    }
+    else if (s == "ISO_IR 127" ||
+             s == "ISO 2022 IR 127")
+    {
+      encoding = Encoding_Arabic;
+    }
+    else if (s == "ISO_IR 126" ||
+             s == "ISO 2022 IR 126")
+    {
+      encoding = Encoding_Greek;
+    }
+    else if (s == "ISO_IR 138" ||
+             s == "ISO 2022 IR 138")
+    {
+      encoding = Encoding_Hebrew;
+    }
+    else if (s == "ISO_IR 166" || s == "ISO 2022 IR 166")
+    {
+      encoding = Encoding_Thai;
+    }
+    else if (s == "ISO_IR 13" || s == "ISO 2022 IR 13")
+    {
+      encoding = Encoding_Japanese;
+    }
+    else if (s == "GB18030")
+    {
+      encoding = Encoding_Chinese;
+    }
+    /*
+      else if (s == "ISO 2022 IR 149")
+      {
+      TODO
+      }
+      else if (s == "ISO 2022 IR 159")
+      {
+      TODO
+      }
+      else if (s == "ISO 2022 IR 87")
+      {
+      TODO
+      }
+    */
+    else
+    {
+      return false;
+    }
+
+    // The encoding was properly detected
+    return true;
+  }
+
+
+  const char* GetMimeType(FileContentType type)
+  {
+    switch (type)
+    {
+      case FileContentType_Dicom:
+        return "application/dicom";
+
+      case FileContentType_DicomAsJson:
+        return "application/json";
+
+      default:
+        return "application/octet-stream";
+    }
+  }
 }
--- a/Core/Enumerations.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Enumerations.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -57,6 +57,8 @@
     ErrorCode_InexistentItem,
     ErrorCode_BadRequest,
     ErrorCode_NetworkProtocol,
+    ErrorCode_SystemCommand,
+    ErrorCode_Database,
 
     // Specific error codes
     ErrorCode_UriSyntax,
@@ -71,7 +73,9 @@
     ErrorCode_InexistentTag,
     ErrorCode_ReadOnly,
     ErrorCode_IncompatibleImageFormat,
-    ErrorCode_IncompatibleImageSize
+    ErrorCode_IncompatibleImageSize,
+    ErrorCode_SharedLibrary,
+    ErrorCode_Plugin
   };
 
   /**
@@ -228,10 +232,56 @@
   };
 
 
+  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/
   enum Encoding
   {
+    Encoding_Ascii,
     Encoding_Utf8,
-    Encoding_Latin1
+    Encoding_Latin1,
+    Encoding_Latin2,
+    Encoding_Latin3,
+    Encoding_Latin4,
+    Encoding_Latin5,                        // Turkish
+    Encoding_Cyrillic,
+    Encoding_Windows1251,                   // Windows-1251 (commonly used for Cyrillic)
+    Encoding_Arabic,
+    Encoding_Greek,
+    Encoding_Hebrew,
+    Encoding_Thai,                          // TIS 620-2533
+    Encoding_Japanese,                      // JIS X 0201 (Shift JIS): Katakana
+    Encoding_Chinese                        // GB18030 - Chinese simplified
+    //Encoding_JapaneseKanji,               // Multibyte - JIS X 0208: Kanji
+    //Encoding_JapaneseSupplementaryKanji,  // Multibyte - JIS X 0212: Supplementary Kanji set
+    //Encoding_Korean,                      // Multibyte - KS X 1001: Hangul and Hanja
+  };
+
+
+  // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.2/
+  enum PhotometricInterpretation
+  {
+    PhotometricInterpretation_ARGB,  // Retired
+    PhotometricInterpretation_CMYK,  // Retired
+    PhotometricInterpretation_HSV,   // Retired
+    PhotometricInterpretation_Monochrome1,
+    PhotometricInterpretation_Monochrome2,
+    PhotometricInterpretation_Palette,
+    PhotometricInterpretation_RGB,
+    PhotometricInterpretation_YBRFull,
+    PhotometricInterpretation_YBRFull422,
+    PhotometricInterpretation_YBRPartial420,
+    PhotometricInterpretation_YBRPartial422,
+    PhotometricInterpretation_YBR_ICT,
+    PhotometricInterpretation_YBR_RCT,
+    PhotometricInterpretation_Unknown
+  };
+
+  enum DicomModule
+  {
+    DicomModule_Patient,
+    DicomModule_Study,
+    DicomModule_Series,
+    DicomModule_Instance,
+    DicomModule_Image
   };
 
 
@@ -249,6 +299,9 @@
 
   enum FileContentType
   {
+    // If you add a value below, insert it in "PluginStorageArea" in
+    // the file "Plugins/Engine/OrthancPlugins.cpp"
+    FileContentType_Unknown = 0,
     FileContentType_Dicom = 1,
     FileContentType_DicomAsJson = 2,
 
@@ -274,9 +327,20 @@
 
   const char* EnumerationToString(ImageFormat format);
 
+  const char* EnumerationToString(Encoding encoding);
+
+  const char* EnumerationToString(PhotometricInterpretation photometric);
+
+  Encoding StringToEncoding(const char* encoding);
+
   ResourceType StringToResourceType(const char* type);
 
   ImageFormat StringToImageFormat(const char* format);
 
   unsigned int GetBytesPerPixel(PixelFormat format);
+
+  bool GetDicomEncoding(Encoding& encoding,
+                        const char* specificCharacterSet);
+
+  const char* GetMimeType(FileContentType type);
 }
--- a/Core/FileStorage/CompressedFileStorageAccessor.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/FileStorage/CompressedFileStorageAccessor.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,6 +36,10 @@
 #include "../OrthancException.h"
 #include "FileStorageAccessor.h"
 #include "../HttpServer/BufferHttpSender.h"
+#include "../Uuid.h"
+
+#include <memory>
+#include <glog/logging.h>
 
 namespace Orthanc
 {
@@ -43,6 +47,8 @@
                                                         size_t size,
                                                         FileContentType type)
   {
+    std::string uuid = Toolbox::GenerateUuid();
+
     std::string md5;
 
     if (storeMD5_)
@@ -54,7 +60,7 @@
     {
     case CompressionType_None:
     {
-      std::string uuid = storage_.Create(data, size);
+      GetStorageArea().Create(uuid.c_str(), data, size, type);
       return FileInfo(uuid, type, size, md5);
     }
 
@@ -70,7 +76,15 @@
         Toolbox::ComputeMD5(compressedMD5, compressed);
       }
 
-      std::string uuid = storage_.Create(compressed);
+      if (compressed.size() > 0)
+      {
+        GetStorageArea().Create(uuid.c_str(), &compressed[0], compressed.size(), type);
+      }
+      else
+      {
+        GetStorageArea().Create(uuid.c_str(), NULL, 0, type);
+      }
+
       return FileInfo(uuid, type, size, md5,
                       CompressionType_Zlib, compressed.size(), compressedMD5);
     }
@@ -80,25 +94,47 @@
     }
   }
 
-  CompressedFileStorageAccessor::CompressedFileStorageAccessor(FileStorage& storage) : 
-    storage_(storage)
+
+  CompressedFileStorageAccessor::CompressedFileStorageAccessor() : 
+    storage_(NULL),
+    compressionType_(CompressionType_None)
   {
-    compressionType_ = CompressionType_None;
+  }
+
+
+  CompressedFileStorageAccessor::CompressedFileStorageAccessor(IStorageArea& storage) : 
+    storage_(&storage),
+    compressionType_(CompressionType_None)
+  {
   }
 
+
+  IStorageArea& CompressedFileStorageAccessor::GetStorageArea()
+  {
+    if (storage_ == NULL)
+    {
+      LOG(ERROR) << "No storage area is currently available";
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    return *storage_;
+  }
+
+
   void CompressedFileStorageAccessor::Read(std::string& content,
-                                           const std::string& uuid)
+                                           const std::string& uuid,
+                                           FileContentType type)
   {
     switch (compressionType_)
     {
     case CompressionType_None:
-      storage_.ReadFile(content, uuid);
+      GetStorageArea().Read(content, uuid, type);
       break;
 
     case CompressionType_Zlib:
     {
       std::string compressed;
-      storage_.ReadFile(compressed, uuid);
+      GetStorageArea().Read(compressed, uuid, type);
       zlib_.Uncompress(content, compressed);
       break;
     }
@@ -108,20 +144,21 @@
     }
   }
 
-  HttpFileSender* CompressedFileStorageAccessor::ConstructHttpFileSender(const std::string& uuid)
+  HttpFileSender* CompressedFileStorageAccessor::ConstructHttpFileSender(const std::string& uuid,
+                                                                         FileContentType type)
   {
     switch (compressionType_)
     {
     case CompressionType_None:
     {
-      FileStorageAccessor uncompressedAccessor(storage_);
-      return uncompressedAccessor.ConstructHttpFileSender(uuid);
+      FileStorageAccessor uncompressedAccessor(GetStorageArea());
+      return uncompressedAccessor.ConstructHttpFileSender(uuid, type);
     }
 
     case CompressionType_Zlib:
     {
       std::string compressed;
-      storage_.ReadFile(compressed, uuid);
+      GetStorageArea().Read(compressed, uuid, type);
 
       std::auto_ptr<BufferHttpSender> sender(new BufferHttpSender);
       zlib_.Uncompress(sender->GetBuffer(), compressed);
@@ -133,4 +170,11 @@
       throw OrthancException(ErrorCode_NotImplemented);
     }
   }
+
+
+  void  CompressedFileStorageAccessor::Remove(const std::string& uuid,
+                                              FileContentType type)
+  {
+    GetStorageArea().Remove(uuid, type);
+  }
 }
--- a/Core/FileStorage/CompressedFileStorageAccessor.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/FileStorage/CompressedFileStorageAccessor.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,8 +32,8 @@
 
 #pragma once
 
+#include "IStorageArea.h"
 #include "StorageAccessor.h"
-#include "FileStorage.h"
 #include "../Compression/ZlibCompressor.h"
 
 namespace Orthanc
@@ -41,7 +41,7 @@
   class CompressedFileStorageAccessor : public StorageAccessor
   {
   private:
-    FileStorage& storage_;
+    IStorageArea* storage_;
     ZlibCompressor zlib_;
     CompressionType compressionType_;
 
@@ -51,7 +51,21 @@
                                    FileContentType type);
 
   public: 
-    CompressedFileStorageAccessor(FileStorage& storage);
+    CompressedFileStorageAccessor();
+
+    CompressedFileStorageAccessor(IStorageArea& storage);
+
+    void SetStorageArea(IStorageArea& storage)
+    {
+      storage_ = &storage;
+    }
+
+    bool HasStorageArea() const
+    {
+      return storage_ != NULL;
+    }
+
+    IStorageArea& GetStorageArea();
 
     void SetCompressionForNextOperations(CompressionType compression)
     {
@@ -64,8 +78,13 @@
     }
 
     virtual void Read(std::string& content,
-                      const std::string& uuid);
+                      const std::string& uuid,
+                      FileContentType type);
 
-    virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid);
+    virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid,
+                                                    FileContentType type);
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type);
   };
 }
--- a/Core/FileStorage/FileInfo.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/FileStorage/FileInfo.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/FileStorage/FileStorage.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,309 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeaders.h"
-#include "FileStorage.h"
-
-// http://stackoverflow.com/questions/1576272/storing-large-number-of-files-in-file-system
-// http://stackoverflow.com/questions/446358/storing-a-large-number-of-images
-
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-#include "../Uuid.h"
-
-#include <boost/filesystem/fstream.hpp>
-#include <glog/logging.h>
-
-static std::string ToString(const boost::filesystem::path& p)
-{
-#if BOOST_HAS_FILESYSTEM_V3 == 1
-  return p.filename().string();
-#else
-  return p.filename();
-#endif
-}
-
-
-namespace Orthanc
-{
-  boost::filesystem::path FileStorage::GetPath(const std::string& uuid) const
-  {
-    namespace fs = boost::filesystem;
-
-    if (!Toolbox::IsUuid(uuid))
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    fs::path path = root_;
-
-    path /= std::string(&uuid[0], &uuid[2]);
-    path /= std::string(&uuid[2], &uuid[4]);
-    path /= uuid;
-
-#if BOOST_HAS_FILESYSTEM_V3 == 1
-    path.make_preferred();
-#endif
-
-    return path;
-  }
-
-  FileStorage::FileStorage(std::string root)
-  {
-    //root_ = boost::filesystem::absolute(root).string();
-    root_ = root;
-
-    Toolbox::CreateDirectory(root);
-  }
-
-  std::string FileStorage::CreateFileWithoutCompression(const void* content, size_t size)
-  {
-    std::string uuid;
-    boost::filesystem::path path;
-    
-    for (;;)
-    {
-      uuid = Toolbox::GenerateUuid();
-      path = GetPath(uuid);
-
-      if (!boost::filesystem::exists(path))
-      {
-        // OK, this is indeed a new file
-        break;
-      }
-
-      // Extremely improbable case: This Uuid has already been created
-      // in the past. Try again.
-    }
-
-    if (boost::filesystem::exists(path.parent_path()))
-    {
-      if (!boost::filesystem::is_directory(path.parent_path()))
-      {
-        throw OrthancException("The subdirectory to be created is already occupied by a regular file");        
-      }
-    }
-    else
-    {
-      if (!boost::filesystem::create_directories(path.parent_path()))
-      {
-        throw OrthancException("Unable to create a subdirectory in the file storage");        
-      }
-    }
-
-    boost::filesystem::ofstream f;
-    f.open(path, std::ofstream::out | std::ios::binary);
-    if (!f.good())
-    {
-      throw OrthancException("Unable to create a new file in the file storage");
-    }
-
-    if (size != 0)
-    {
-      f.write(static_cast<const char*>(content), size);
-      if (!f.good())
-      {
-        f.close();
-        throw OrthancException("Unable to write to the new file in the file storage");
-      }
-    }
-
-    f.close();
-
-    return uuid;
-  } 
-
-
-  std::string FileStorage::Create(const void* content, size_t size)
-  {
-    if (!HasBufferCompressor() || size == 0)
-    {
-      return CreateFileWithoutCompression(content, size);
-    }
-    else
-    {
-      std::string compressed;
-      compressor_->Compress(compressed, content, size);
-      assert(compressed.size() > 0);
-      return CreateFileWithoutCompression(&compressed[0], compressed.size());
-    }
-  }
-
-
-  std::string FileStorage::Create(const std::vector<uint8_t>& content)
-  {
-    if (content.size() == 0)
-      return Create(NULL, 0);
-    else
-      return Create(&content[0], content.size());
-  }
-
-  std::string FileStorage::Create(const std::string& content)
-  {
-    if (content.size() == 0)
-      return Create(NULL, 0);
-    else
-      return Create(&content[0], content.size());
-  }
-
-  void FileStorage::ReadFile(std::string& content,
-                             const std::string& uuid) const
-  {
-    content.clear();
-
-    if (HasBufferCompressor())
-    {
-      std::string compressed;
-      Toolbox::ReadFile(compressed, ToString(GetPath(uuid)));
-
-      if (compressed.size() != 0)
-      {
-        compressor_->Uncompress(content, compressed);
-      }
-    }
-    else
-    {
-      Toolbox::ReadFile(content, GetPath(uuid).string());
-    }
-  }
-
-
-  uintmax_t FileStorage::GetCompressedSize(const std::string& uuid) const
-  {
-    boost::filesystem::path path = GetPath(uuid);
-    return boost::filesystem::file_size(path);
-  }
-
-
-
-  void FileStorage::ListAllFiles(std::set<std::string>& result) const
-  {
-    namespace fs = boost::filesystem;
-
-    result.clear();
-
-    if (fs::exists(root_) && fs::is_directory(root_))
-    {
-      for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current)
-      {
-        if (fs::is_regular_file(current->status()))
-        {
-          try
-          {
-            fs::path d = current->path();
-            std::string uuid = ToString(d);
-            if (Toolbox::IsUuid(uuid))
-            {
-              fs::path p0 = d.parent_path().parent_path().parent_path();
-              std::string p1 = ToString(d.parent_path().parent_path());
-              std::string p2 = ToString(d.parent_path());
-              if (p1.length() == 2 &&
-                  p2.length() == 2 &&
-                  p1 == uuid.substr(0, 2) &&
-                  p2 == uuid.substr(2, 2) &&
-                  p0 == root_)
-              {
-                result.insert(uuid);
-              }
-            }
-          }
-          catch (fs::filesystem_error)
-          {
-          }
-        }
-      }
-    }
-  }
-
-
-  void FileStorage::Clear()
-  {
-    namespace fs = boost::filesystem;
-    typedef std::set<std::string> List;
-
-    List result;
-    ListAllFiles(result);
-
-    for (List::const_iterator it = result.begin(); it != result.end(); ++it)
-    {
-      Remove(*it);
-    }
-  }
-
-
-  void FileStorage::Remove(const std::string& uuid)
-  {
-    LOG(INFO) << "Deleting file " << uuid;
-    namespace fs = boost::filesystem;
-
-    fs::path p = GetPath(uuid);
-
-    try
-    {
-      fs::remove(p);
-    }
-    catch (...)
-    {
-      // Ignore the error
-    }
-
-    // Remove the two parent directories, ignoring the error code if
-    // these directories are not empty
-
-    try
-    {
-#if BOOST_HAS_FILESYSTEM_V3 == 1
-      boost::system::error_code err;
-      fs::remove(p.parent_path(), err);
-      fs::remove(p.parent_path().parent_path(), err);
-#else
-      fs::remove(p.parent_path());
-      fs::remove(p.parent_path().parent_path());
-#endif
-    }
-    catch (...)
-    {
-      // Ignore the error
-    }
-  }
-
-
-  uintmax_t FileStorage::GetCapacity() const
-  {
-    return boost::filesystem::space(root_).capacity;
-  }
-
-  uintmax_t FileStorage::GetAvailableSpace() const
-  {
-    return boost::filesystem::space(root_).available;
-  }
-}
--- a/Core/FileStorage/FileStorage.h	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 <boost/filesystem.hpp>
-#include <set>
-
-#include "../Compression/BufferCompressor.h"
-
-namespace Orthanc
-{
-  class FileStorage : public boost::noncopyable
-  {
-    // TODO REMOVE THIS
-    friend class FilesystemHttpSender;
-    friend class FileStorageAccessor;
-
-  private:
-    std::auto_ptr<BufferCompressor> compressor_;
-
-    boost::filesystem::path root_;
-
-    boost::filesystem::path GetPath(const std::string& uuid) const;
-
-    std::string CreateFileWithoutCompression(const void* content, size_t size);
-
-  public:
-    FileStorage(std::string root);
-
-    void SetBufferCompressor(BufferCompressor* compressor)  // Takes the ownership
-    {
-      compressor_.reset(compressor);
-    }
-
-    bool HasBufferCompressor() const
-    {
-      return compressor_.get() != NULL;
-    }
-
-    std::string Create(const void* content, size_t size);
-
-    std::string Create(const std::vector<uint8_t>& content);
-
-    std::string Create(const std::string& content);
-
-    void ReadFile(std::string& content,
-                  const std::string& uuid) const;
-
-    void ListAllFiles(std::set<std::string>& result) const;
-
-    uintmax_t GetCompressedSize(const std::string& uuid) const;
-
-    void Clear();
-
-    void Remove(const std::string& uuid);
-
-    uintmax_t GetCapacity() const;
-
-    uintmax_t GetAvailableSpace() const;
-
-    std::string GetPath() const
-    {
-      return root_.string();
-    }
-  };
-}
--- a/Core/FileStorage/FileStorageAccessor.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/FileStorage/FileStorageAccessor.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,6 +33,12 @@
 #include "../PrecompiledHeaders.h"
 #include "FileStorageAccessor.h"
 
+#include "../HttpServer/BufferHttpSender.h"
+#include "../Uuid.h"
+
+#include <memory>
+#include <stdio.h>
+
 namespace Orthanc
 {
   FileInfo FileStorageAccessor::WriteInternal(const void* data,
@@ -46,6 +52,21 @@
       Toolbox::ComputeMD5(md5, data, size);
     }
 
-    return FileInfo(storage_.Create(data, size), type, size, md5);
+    std::string uuid = Toolbox::GenerateUuid();
+    storage_.Create(uuid.c_str(), data, size, type);
+
+    return FileInfo(uuid, type, size, md5);
   }
+
+
+  HttpFileSender* FileStorageAccessor::ConstructHttpFileSender(const std::string& uuid,
+                                                               FileContentType type)
+  {
+    std::auto_ptr<BufferHttpSender> sender(new BufferHttpSender);
+
+    storage_.Read(sender->GetBuffer(), uuid, type);
+      
+    return sender.release();
+  }
+
 }
--- a/Core/FileStorage/FileStorageAccessor.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/FileStorage/FileStorageAccessor.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,15 +33,14 @@
 #pragma once
 
 #include "StorageAccessor.h"
-#include "FileStorage.h"
-#include "../HttpServer/FilesystemHttpSender.h"
+#include "IStorageArea.h"
 
 namespace Orthanc
 {
   class FileStorageAccessor : public StorageAccessor
   {
   private:
-    FileStorage& storage_;
+    IStorageArea& storage_;
     
   protected:
     virtual FileInfo WriteInternal(const void* data,
@@ -49,19 +48,24 @@
                                    FileContentType type);
 
   public:
-    FileStorageAccessor(FileStorage& storage) : storage_(storage)
+    FileStorageAccessor(IStorageArea& storage) : storage_(storage)
     {
     }
 
     virtual void Read(std::string& content,
-                      const std::string& uuid)
+                      const std::string& uuid,
+                      FileContentType type)
     {
-      storage_.ReadFile(content, uuid);
+      storage_.Read(content, uuid, type);
     }
 
-    virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid)
+    virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid,
+                                                    FileContentType type);
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type)
     {
-      return new FilesystemHttpSender(storage_.GetPath(uuid));
+      storage_.Remove(uuid, type);
     }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/FileStorage/FilesystemStorage.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,259 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "FilesystemStorage.h"
+
+// http://stackoverflow.com/questions/1576272/storing-large-number-of-files-in-file-system
+// http://stackoverflow.com/questions/446358/storing-a-large-number-of-images
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+#include "../Uuid.h"
+
+#include <boost/filesystem/fstream.hpp>
+#include <glog/logging.h>
+
+static std::string ToString(const boost::filesystem::path& p)
+{
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+  return p.filename().string();
+#else
+  return p.filename();
+#endif
+}
+
+
+namespace Orthanc
+{
+  boost::filesystem::path FilesystemStorage::GetPath(const std::string& uuid) const
+  {
+    namespace fs = boost::filesystem;
+
+    if (!Toolbox::IsUuid(uuid))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    fs::path path = root_;
+
+    path /= std::string(&uuid[0], &uuid[2]);
+    path /= std::string(&uuid[2], &uuid[4]);
+    path /= uuid;
+
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+    path.make_preferred();
+#endif
+
+    return path;
+  }
+
+  FilesystemStorage::FilesystemStorage(std::string root)
+  {
+    //root_ = boost::filesystem::absolute(root).string();
+    root_ = root;
+
+    Toolbox::CreateDirectory(root);
+  }
+
+  void FilesystemStorage::Create(const std::string& uuid,
+                                 const void* content, 
+                                 size_t size,
+                                 FileContentType /*type*/)
+  {
+    boost::filesystem::path path;
+    
+    path = GetPath(uuid);
+
+    if (boost::filesystem::exists(path))
+    {
+      // Extremely unlikely case: This Uuid has already been created
+      // in the past.
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (boost::filesystem::exists(path.parent_path()))
+    {
+      if (!boost::filesystem::is_directory(path.parent_path()))
+      {
+        throw OrthancException("The subdirectory to be created is already occupied by a regular file");        
+      }
+    }
+    else
+    {
+      if (!boost::filesystem::create_directories(path.parent_path()))
+      {
+        throw OrthancException("Unable to create a subdirectory in the file storage");        
+      }
+    }
+
+    boost::filesystem::ofstream f;
+    f.open(path, std::ofstream::out | std::ios::binary);
+    if (!f.good())
+    {
+      throw OrthancException("Unable to create a new file in the file storage");
+    }
+
+    if (size != 0)
+    {
+      f.write(static_cast<const char*>(content), size);
+      if (!f.good())
+      {
+        f.close();
+        throw OrthancException("Unable to write to the new file in the file storage");
+      }
+    }
+
+    f.close();
+  }
+
+
+  void FilesystemStorage::Read(std::string& content,
+                               const std::string& uuid,
+                               FileContentType /*type*/)
+  {
+    content.clear();
+    Toolbox::ReadFile(content, GetPath(uuid).string());
+  }
+
+
+  uintmax_t FilesystemStorage::GetSize(const std::string& uuid) const
+  {
+    boost::filesystem::path path = GetPath(uuid);
+    return boost::filesystem::file_size(path);
+  }
+
+
+
+  void FilesystemStorage::ListAllFiles(std::set<std::string>& result) const
+  {
+    namespace fs = boost::filesystem;
+
+    result.clear();
+
+    if (fs::exists(root_) && fs::is_directory(root_))
+    {
+      for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current)
+      {
+        if (fs::is_regular_file(current->status()))
+        {
+          try
+          {
+            fs::path d = current->path();
+            std::string uuid = ToString(d);
+            if (Toolbox::IsUuid(uuid))
+            {
+              fs::path p0 = d.parent_path().parent_path().parent_path();
+              std::string p1 = ToString(d.parent_path().parent_path());
+              std::string p2 = ToString(d.parent_path());
+              if (p1.length() == 2 &&
+                  p2.length() == 2 &&
+                  p1 == uuid.substr(0, 2) &&
+                  p2 == uuid.substr(2, 2) &&
+                  p0 == root_)
+              {
+                result.insert(uuid);
+              }
+            }
+          }
+          catch (fs::filesystem_error)
+          {
+          }
+        }
+      }
+    }
+  }
+
+
+  void FilesystemStorage::Clear()
+  {
+    namespace fs = boost::filesystem;
+    typedef std::set<std::string> List;
+
+    List result;
+    ListAllFiles(result);
+
+    for (List::const_iterator it = result.begin(); it != result.end(); ++it)
+    {
+      Remove(*it, FileContentType_Unknown /*ignored in this class*/);
+    }
+  }
+
+
+  void FilesystemStorage::Remove(const std::string& uuid,
+                                 FileContentType /*type*/)
+  {
+    LOG(INFO) << "Deleting file " << uuid;
+    namespace fs = boost::filesystem;
+
+    fs::path p = GetPath(uuid);
+
+    try
+    {
+      fs::remove(p);
+    }
+    catch (...)
+    {
+      // Ignore the error
+    }
+
+    // Remove the two parent directories, ignoring the error code if
+    // these directories are not empty
+
+    try
+    {
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+      boost::system::error_code err;
+      fs::remove(p.parent_path(), err);
+      fs::remove(p.parent_path().parent_path(), err);
+#else
+      fs::remove(p.parent_path());
+      fs::remove(p.parent_path().parent_path());
+#endif
+    }
+    catch (...)
+    {
+      // Ignore the error
+    }
+  }
+
+
+  uintmax_t FilesystemStorage::GetCapacity() const
+  {
+    return boost::filesystem::space(root_).capacity;
+  }
+
+  uintmax_t FilesystemStorage::GetAvailableSpace() const
+  {
+    return boost::filesystem::space(root_).available;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/FileStorage/FilesystemStorage.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "IStorageArea.h"
+
+#include <boost/filesystem.hpp>
+#include <set>
+
+namespace Orthanc
+{
+  class FilesystemStorage : public IStorageArea
+  {
+    // TODO REMOVE THIS
+    friend class FilesystemHttpSender;
+    friend class FileStorageAccessor;
+
+  private:
+    boost::filesystem::path root_;
+
+    boost::filesystem::path GetPath(const std::string& uuid) const;
+
+  public:
+    FilesystemStorage(std::string root);
+
+    virtual void Create(const std::string& uuid,
+                        const void* content, 
+                        size_t size,
+                        FileContentType type);
+
+    virtual void Read(std::string& content,
+                      const std::string& uuid,
+                      FileContentType type);
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type);
+
+    void ListAllFiles(std::set<std::string>& result) const;
+
+    uintmax_t GetSize(const std::string& uuid) const;
+
+    void Clear();
+
+    uintmax_t GetCapacity() const;
+
+    uintmax_t GetAvailableSpace() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/FileStorage/IStorageArea.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,60 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "../Enumerations.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IStorageArea : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageArea()
+    {
+    }
+
+    virtual void Create(const std::string& uuid,
+                        const void* content,
+                        size_t size,
+                        FileContentType type) = 0;
+
+    virtual void Read(std::string& content,
+                      const std::string& uuid,
+                      FileContentType type) = 0;
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type) = 0;
+  };
+}
--- a/Core/FileStorage/StorageAccessor.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/FileStorage/StorageAccessor.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/FileStorage/StorageAccessor.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/FileStorage/StorageAccessor.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -85,8 +85,13 @@
                    FileContentType type);
 
     virtual void Read(std::string& content,
-                      const std::string& uuid) = 0;
+                      const std::string& uuid,
+                      FileContentType type) = 0;
 
-    virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid) = 0;
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type) = 0;
+
+    virtual HttpFileSender* ConstructHttpFileSender(const std::string& uuid,
+                                                    FileContentType type) = 0;
   };
 }
--- a/Core/HttpClient.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpClient.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -109,6 +109,7 @@
     method_ = HttpMethod_Get;
     lastStatus_ = HttpStatus_200_Ok;
     isVerbose_ = false;
+    timeout_ = 0;
   }
 
 
@@ -161,13 +162,39 @@
     answer.clear();
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str()));
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer));
+
+    // Reset the parameters from previous calls to Apply()
     CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, NULL));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 0L));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 0L));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 0L));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, NULL));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, NULL));
+
+    // Set timeouts
+    if (timeout_ <= 0)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, 10));  /* default: 10 seconds */
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, 10));  /* default: 10 seconds */
+    }
+    else
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, timeout_));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, timeout_));
+    }
 
     if (credentials_.size() != 0)
     {
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_USERPWD, credentials_.c_str()));
     }
 
+    if (proxy_.size() != 0)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, proxy_.c_str()));
+    }
+
     switch (method_)
     {
     case HttpMethod_Get:
@@ -177,7 +204,31 @@
     case HttpMethod_Post:
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->postHeaders_));
+      break;
 
+    case HttpMethod_Delete:
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 1L));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "DELETE"));
+      break;
+
+    case HttpMethod_Put:
+      // http://stackoverflow.com/a/7570281/881731: Don't use
+      // CURLOPT_PUT if there is a body
+
+      // CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L));
+
+      curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "PUT"); /* !!! */
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, pimpl_->postHeaders_));      
+      break;
+
+    default:
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+
+    if (method_ == HttpMethod_Post ||
+        method_ == HttpMethod_Put)
+    {
       if (postData_.size() > 0)
       {
         CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, postData_.c_str()));
@@ -188,21 +239,8 @@
         CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL));
         CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0));
       }
-
-      break;
-
-    case HttpMethod_Delete:
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 1L));
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "DELETE"));
-      break;
+    }
 
-    case HttpMethod_Put:
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L));
-      break;
-
-    default:
-      throw OrthancException(ErrorCode_InternalError);
-    }
 
     // Do the actual request
     CheckCode(curl_easy_perform(pimpl_->curl_));
--- a/Core/HttpClient.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpClient.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -52,6 +52,8 @@
     HttpStatus lastStatus_;
     std::string postData_;
     bool isVerbose_;
+    long timeout_;
+    std::string proxy_;
 
     void Setup();
 
@@ -89,6 +91,21 @@
       return method_;
     }
 
+    void SetTimeout(long seconds)
+    {
+      timeout_ = seconds;
+    }
+
+    long GetTimeout() const
+    {
+      return timeout_;
+    }
+
+    void SetPostData(const std::string& data)
+    {
+      postData_ = data;
+    }
+
     std::string& AccessPostData()
     {
       return postData_;
@@ -118,6 +135,11 @@
     void SetCredentials(const char* username,
                         const char* password);
 
+    void SetProxy(const std::string& proxy)
+    {
+      proxy_ = proxy;
+    }
+
     static void GlobalInitialize();
   
     static void GlobalFinalize();
--- a/Core/HttpServer/BufferHttpSender.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/BufferHttpSender.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -49,7 +49,9 @@
     virtual bool SendData(HttpOutput& output)
     {
       if (buffer_.size())
-        output.Send(&buffer_[0], buffer_.size());
+      {
+        output.SendBody(&buffer_[0], buffer_.size());
+      }
 
       return true;
     }
--- a/Core/HttpServer/EmbeddedResourceHttpHandler.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/EmbeddedResourceHttpHandler.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -51,24 +51,24 @@
   }
 
 
-  bool EmbeddedResourceHttpHandler::IsServedUri(const UriComponents& uri)
-  {
-    return Toolbox::IsChildUri(baseUri_, uri);
-  }
-
-
-  void EmbeddedResourceHttpHandler::Handle(
+  bool EmbeddedResourceHttpHandler::Handle(
     HttpOutput& output,
     HttpMethod method,
     const UriComponents& uri,
     const Arguments& headers,
-    const Arguments& arguments,
+    const GetArguments& arguments,
     const std::string&)
   {
+    if (!Toolbox::IsChildUri(baseUri_, uri))
+    {
+      // This URI is not served by this handler
+      return false;
+    }
+
     if (method != HttpMethod_Get)
     {
-      output.SendMethodNotAllowedError("GET");
-      return;
+      output.SendMethodNotAllowed("GET");
+      return true;
     }
 
     std::string resourcePath = Toolbox::FlattenUri(uri, baseUri_.size());
@@ -78,12 +78,16 @@
     {
       const void* buffer = EmbeddedResources::GetDirectoryResourceBuffer(resourceId_, resourcePath.c_str());
       size_t size = EmbeddedResources::GetDirectoryResourceSize(resourceId_, resourcePath.c_str());
-      output.AnswerBufferWithContentType(buffer, size, contentType);
+
+      output.SetContentType(contentType.c_str());
+      output.SendBody(buffer, size);
     }
     catch (OrthancException&)
     {
       LOG(WARNING) << "Unable to find HTTP resource: " << resourcePath;
-      output.SendHeader(HttpStatus_404_NotFound);
+      output.SendStatus(HttpStatus_404_NotFound);
     }
+
+    return true;
   } 
 }
--- a/Core/HttpServer/EmbeddedResourceHttpHandler.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/EmbeddedResourceHttpHandler.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -50,14 +50,12 @@
       const std::string& baseUri,
       EmbeddedResources::DirectoryResourceId resourceId);
 
-    virtual bool IsServedUri(const UriComponents& uri);
-
-    virtual void Handle(
+    virtual bool Handle(
       HttpOutput& output,
       HttpMethod method,
       const UriComponents& uri,
       const Arguments& headers,
-      const Arguments& arguments,
+      const GetArguments& arguments,
       const std::string&);
   };
 }
--- a/Core/HttpServer/FilesystemHttpHandler.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/FilesystemHttpHandler.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -55,16 +55,18 @@
   {
     namespace fs = boost::filesystem;
 
-    output.SendOkHeader("text/html", false, 0, NULL);
-    output.SendString("<html>");
-    output.SendString("  <body>");
-    output.SendString("    <h1>Subdirectories</h1>");
-    output.SendString("    <ul>");
+    output.SetContentType("text/html");
+
+    std::string s;
+    s += "<html>";
+    s += "  <body>";
+    s += "    <h1>Subdirectories</h1>";
+    s += "    <ul>";
 
     if (uri.size() > 0)
     {
       std::string h = Toolbox::FlattenUri(uri) + "/..";
-      output.SendString("<li><a href=\"" + h + "\">..</a></li>");
+      s += "<li><a href=\"" + h + "\">..</a></li>";
     }
 
     fs::directory_iterator end;
@@ -78,12 +80,12 @@
 
       std::string h = Toolbox::FlattenUri(uri) + "/" + f;
       if (fs::is_directory(it->status()))
-        output.SendString("<li><a href=\"" + h + "\">" + f + "</a></li>");
+        s += "<li><a href=\"" + h + "\">" + f + "</a></li>";
     }      
 
-    output.SendString("    </ul>");      
-    output.SendString("    <h1>Files</h1>");
-    output.SendString("    <ul>");
+    s += "    </ul>";      
+    s += "    <h1>Files</h1>";
+    s += "    <ul>";
 
     for (fs::directory_iterator it(p) ; it != end; ++it)
     {
@@ -95,12 +97,14 @@
 
       std::string h = Toolbox::FlattenUri(uri) + "/" + f;
       if (fs::is_regular_file(it->status()))
-        output.SendString("<li><a href=\"" + h + "\">" + f + "</a></li>");
+        s += "<li><a href=\"" + h + "\">" + f + "</a></li>";
     }      
 
-    output.SendString("    </ul>");
-    output.SendString("  </body>");
-    output.SendString("</html>");
+    s += "    </ul>";
+    s += "  </body>";
+    s += "</html>";
+
+    output.SendBody(s);
   }
 
 
@@ -120,24 +124,24 @@
   }
 
 
-  bool FilesystemHttpHandler::IsServedUri(const UriComponents& uri)
-  {
-    return Toolbox::IsChildUri(pimpl_->baseUri_, uri);
-  }
-
-
-  void FilesystemHttpHandler::Handle(
+  bool FilesystemHttpHandler::Handle(
     HttpOutput& output,
     HttpMethod method,
     const UriComponents& uri,
     const Arguments& headers,
-    const Arguments& arguments,
+    const GetArguments& arguments,
     const std::string&)
   {
+    if (!Toolbox::IsChildUri(pimpl_->baseUri_, uri))
+    {
+      // This URI is not served by this handler
+      return false;
+    }
+
     if (method != HttpMethod_Get)
     {
-      output.SendMethodNotAllowedError("GET");
-      return;
+      output.SendMethodNotAllowed("GET");
+      return true;
     }
 
     namespace fs = boost::filesystem;
@@ -162,7 +166,9 @@
     }
     else
     {
-      output.SendHeader(HttpStatus_404_NotFound);
+      output.SendStatus(HttpStatus_404_NotFound);
     }
+
+    return true;
   } 
 }
--- a/Core/HttpServer/FilesystemHttpHandler.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/FilesystemHttpHandler.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -52,14 +52,12 @@
     FilesystemHttpHandler(const std::string& baseUri,
                           const std::string& root);
 
-    virtual bool IsServedUri(const UriComponents& uri);
-
-    virtual void Handle(
+    virtual bool Handle(
       HttpOutput& output,
       HttpMethod method,
       const UriComponents& uri,
       const Arguments& headers,
-      const Arguments& arguments,
+      const GetArguments& arguments,
       const std::string&);
 
     bool IsListDirectoryContent() const
--- a/Core/HttpServer/FilesystemHttpSender.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/FilesystemHttpSender.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -73,7 +73,7 @@
       }
       else
       {
-        output.Send(&buffer[0], nbytes);
+        output.SendBody(&buffer[0], nbytes);
       }
     }
 
@@ -94,7 +94,7 @@
     Setup();
   }
 
-  FilesystemHttpSender::FilesystemHttpSender(const FileStorage& storage,
+  FilesystemHttpSender::FilesystemHttpSender(const FilesystemStorage& storage,
                                              const std::string& uuid)
   {
     path_ = storage.GetPath(uuid).string();
--- a/Core/HttpServer/FilesystemHttpSender.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/FilesystemHttpSender.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,7 +32,7 @@
 #pragma once
 
 #include "HttpFileSender.h"
-#include "../FileStorage/FileStorage.h"
+#include "../FileStorage/FilesystemStorage.h"
 
 namespace Orthanc
 {
@@ -53,7 +53,7 @@
 
     FilesystemHttpSender(const boost::filesystem::path& path);
 
-    FilesystemHttpSender(const FileStorage& storage,
+    FilesystemHttpSender(const FilesystemStorage& storage,
                          const std::string& uuid);
   };
 }
--- a/Core/HttpServer/HttpFileSender.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/HttpFileSender.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,13 +33,25 @@
 #include "../PrecompiledHeaders.h"
 #include "HttpFileSender.h"
 
+#include "../OrthancException.h"
+
 #include <boost/lexical_cast.hpp>
 
 namespace Orthanc
 {
   void HttpFileSender::SendHeader(HttpOutput& output)
   {
-    output.SendOkHeader(contentType_.c_str(), true, GetFileSize(), downloadFilename_.c_str());
+    if (contentType_.size() > 0)
+    {
+      output.SetContentType(contentType_.c_str());
+    }
+
+    if (downloadFilename_.size() > 0)
+    {
+      output.SetContentFilename(downloadFilename_.c_str());
+    }
+
+    output.SetContentLength(GetFileSize());
   }
 
   void HttpFileSender::Send(HttpOutput& output)
@@ -48,7 +60,8 @@
 
     if (!SendData(output))
     {
-      output.SendHeader(HttpStatus_500_InternalServerError);
+      throw OrthancException(ErrorCode_InternalError);
+      //output.SendHeader(HttpStatus_500_InternalServerError);
     }
   }
 }
--- a/Core/HttpServer/HttpFileSender.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/HttpFileSender.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/HttpServer/HttpHandler.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/HttpHandler.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,10 +34,12 @@
 #include "HttpHandler.h"
 
 #include <string.h>
+#include <iostream>
+
 
 namespace Orthanc
 {
-  static void SplitGETNameValue(HttpHandler::Arguments& result,
+  static void SplitGETNameValue(HttpHandler::GetArguments& result,
                                 const char* start,
                                 const char* end)
   {
@@ -58,11 +60,12 @@
     Toolbox::UrlDecode(name);
     Toolbox::UrlDecode(value);
 
-    result.insert(std::make_pair(name, value));
+    result.push_back(std::make_pair(name, value));
   }
 
 
-  void HttpHandler::ParseGetQuery(HttpHandler::Arguments& result, const char* query)
+  void HttpHandler::ParseGetArguments(HttpHandler::GetArguments& result, 
+                                      const char* query)
   {
     const char* pos = query;
 
@@ -84,7 +87,25 @@
   }
 
 
+  void  HttpHandler::ParseGetQuery(UriComponents& uri,
+                                   HttpHandler::GetArguments& getArguments, 
+                                   const char* query)
+  {
+    const char *questionMark = ::strchr(query, '?');
+    if (questionMark == NULL)
+    {
+      // No question mark in the string
+      Toolbox::SplitUriComponents(uri, query);
+      getArguments.clear();
+    }
+    else
+    {
+      Toolbox::SplitUriComponents(uri, std::string(query, questionMark));
+      HttpHandler::ParseGetArguments(getArguments, questionMark + 1);
+    }    
+  }
 
+ 
   std::string HttpHandler::GetArgument(const Arguments& getArguments,
                                        const std::string& name,
                                        const std::string& defaultValue)
@@ -101,6 +122,22 @@
   }
 
 
+  std::string HttpHandler::GetArgument(const GetArguments& getArguments,
+                                       const std::string& name,
+                                       const std::string& defaultValue)
+  {
+    for (size_t i = 0; i < getArguments.size(); i++)
+    {
+      if (getArguments[i].first == name)
+      {
+        return getArguments[i].second;
+      }
+    }
+
+    return defaultValue;
+  }
+
+
 
   void HttpHandler::ParseCookies(HttpHandler::Arguments& result, 
                                  const HttpHandler::Arguments& httpHeaders)
@@ -139,4 +176,16 @@
       }
     }
   }
+
+
+  void HttpHandler::CompileGetArguments(Arguments& compiled,
+                                        const GetArguments& source)
+  {
+    compiled.clear();
+
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      compiled[source[i].first] = source[i].second;
+    }
+  }
 }
--- a/Core/HttpServer/HttpHandler.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/HttpHandler.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -44,29 +44,39 @@
   class HttpHandler
   {
   public:
-    typedef std::map<std::string, std::string> Arguments;
+    typedef std::map<std::string, std::string>                  Arguments;
+    typedef std::vector< std::pair<std::string, std::string> >  GetArguments;
 
     virtual ~HttpHandler()
     {
     }
 
-    virtual bool IsServedUri(const UriComponents& uri) = 0;
-
-    virtual void Handle(HttpOutput& output,
+    virtual bool Handle(HttpOutput& output,
                         HttpMethod method,
                         const UriComponents& uri,
                         const Arguments& headers,
-                        const Arguments& getArguments,
+                        const GetArguments& getArguments,
                         const std::string& postData) = 0;
 
-    static void ParseGetQuery(HttpHandler::Arguments& result, 
+    static void ParseGetArguments(HttpHandler::GetArguments& result, 
+                                  const char* query);
+
+    static void ParseGetQuery(UriComponents& uri,
+                              HttpHandler::GetArguments& getArguments, 
                               const char* query);
 
     static std::string GetArgument(const Arguments& getArguments,
                                    const std::string& name,
                                    const std::string& defaultValue);
 
+    static std::string GetArgument(const GetArguments& getArguments,
+                                   const std::string& name,
+                                   const std::string& defaultValue);
+
     static void ParseCookies(HttpHandler::Arguments& result, 
                              const HttpHandler::Arguments& httpHeaders);
+
+    static void CompileGetArguments(Arguments& compiled,
+                                    const GetArguments& source);
   };
 }
--- a/Core/HttpServer/HttpOutput.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/HttpOutput.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,163 +36,238 @@
 #include <iostream>
 #include <vector>
 #include <stdio.h>
+#include <glog/logging.h>
 #include <boost/lexical_cast.hpp>
 #include "../OrthancException.h"
 #include "../Toolbox.h"
 
 namespace Orthanc
 {
-  void HttpOutput::SendString(const std::string& s)
+  HttpOutput::StateMachine::StateMachine(IHttpOutputStream& stream,
+                                         bool isKeepAlive) : 
+    stream_(stream),
+    state_(State_WritingHeader),
+    status_(HttpStatus_200_Ok),
+    hasContentLength_(false),
+    contentPosition_(0),
+    keepAlive_(isKeepAlive)
   {
-    if (s.size() > 0)
-    {
-      Send(&s[0], s.size());
-    }
   }
 
-  void HttpOutput::PrepareOkHeader(Header& header,
-                                   const char* contentType,
-                                   bool hasContentLength,
-                                   uint64_t contentLength,
-                                   const char* contentFilename)
+  HttpOutput::StateMachine::~StateMachine()
   {
-    header.clear();
-
-    if (contentType && contentType[0] != '\0')
-    {
-      header.push_back(std::make_pair("Content-Type", std::string(contentType)));
-    }
-
-    if (hasContentLength)
-    {
-      header.push_back(std::make_pair("Content-Length", boost::lexical_cast<std::string>(contentLength)));
-    }
-
-    if (contentFilename && contentFilename[0] != '\0')
+    if (state_ != State_Done)
     {
-      std::string attachment = "attachment; filename=\"" + std::string(contentFilename) + "\"";
-      header.push_back(std::make_pair("Content-Disposition", attachment));
-    }
-  }
-
-  void HttpOutput::SendOkHeader(const char* contentType,
-                                bool hasContentLength,
-                                uint64_t contentLength,
-                                const char* contentFilename)
-  {
-    Header header;
-    PrepareOkHeader(header, contentType, hasContentLength, contentLength, contentFilename);
-    SendOkHeader(header);
-  }
-
-  void HttpOutput::SendOkHeader(const Header& header)
-  {
-    std::string s = "HTTP/1.1 200 OK\r\n";
-
-    for (Header::const_iterator 
-           it = header.begin(); it != header.end(); ++it)
-    {
-      s += it->first + ": " + it->second + "\r\n";
+      //asm volatile ("int3;");
+      //LOG(ERROR) << "This HTTP answer does not contain any body";
     }
 
-    s += "\r\n";
-
-    Send(&s[0], s.size());
-  }
-
-
-  void HttpOutput::SendMethodNotAllowedError(const std::string& allowed)
-  {
-    std::string s = 
-      "HTTP/1.1 405 " + std::string(EnumerationToString(HttpStatus_405_MethodNotAllowed)) +
-      "\r\nAllow: " + allowed + 
-      "\r\n\r\n";
-    Send(&s[0], s.size());
-  }
-
-
-  void HttpOutput::SendHeader(HttpStatus status)
-  {
-    if (status == HttpStatus_200_Ok ||
-        status == HttpStatus_405_MethodNotAllowed)
+    if (hasContentLength_ && contentPosition_ != contentLength_)
     {
-      throw OrthancException("Please use the dedicated methods to this HTTP status code in HttpOutput");
-    }
-    
-    SendHeaderInternal(status);
-  }
-
-
-  void HttpOutput::SendHeaderInternal(HttpStatus status)
-  {
-    std::string s = "HTTP/1.1 " + 
-      boost::lexical_cast<std::string>(status) +
-      " " + std::string(EnumerationToString(status)) +
-      "\r\n\r\n";
-    Send(&s[0], s.size());
-  }
-
-
-  void HttpOutput::AnswerBufferWithContentType(const std::string& buffer,
-                                               const std::string& contentType)
-  {
-    SendOkHeader(contentType.c_str(), true, buffer.size(), NULL);
-    SendString(buffer);
-  }
-
-
-  void HttpOutput::PrepareCookies(Header& header,
-                                  const HttpHandler::Arguments& cookies)
-  {
-    for (HttpHandler::Arguments::const_iterator it = cookies.begin();
-         it != cookies.end(); ++it)
-    {
-      header.push_back(std::make_pair("Set-Cookie", it->first + "=" + it->second));
+      LOG(ERROR) << "This HTTP answer has not sent the proper number of bytes in its body";
     }
   }
 
 
-  void HttpOutput::AnswerBufferWithContentType(const std::string& buffer,
-                                               const std::string& contentType,
-                                               const HttpHandler::Arguments& cookies)
+  void HttpOutput::StateMachine::SetHttpStatus(HttpStatus status)
+  {
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    status_ = status;
+  }
+
+
+  void HttpOutput::StateMachine::SetContentLength(uint64_t length)
   {
-    Header header;
-    PrepareOkHeader(header, contentType.c_str(), true, buffer.size(), NULL);
-    PrepareCookies(header, cookies);
-    SendOkHeader(header);
-    SendString(buffer);
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    hasContentLength_ = true;
+    contentLength_ = length;
+  }
+
+  void HttpOutput::StateMachine::SetContentType(const char* contentType)
+  {
+    AddHeader("Content-Type", contentType);
+  }
+
+  void HttpOutput::StateMachine::SetContentFilename(const char* filename)
+  {
+    // TODO Escape double quotes
+    AddHeader("Content-Disposition", "filename=\"" + std::string(filename) + "\"");
+  }
+
+  void HttpOutput::StateMachine::SetCookie(const std::string& cookie,
+                                           const std::string& value)
+  {
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    // TODO Escape "=" characters
+    AddHeader("Set-Cookie", cookie + "=" + value);
   }
 
 
-  void HttpOutput::AnswerBufferWithContentType(const void* buffer,
-                                               size_t size,
-                                               const std::string& contentType)
+  void HttpOutput::StateMachine::AddHeader(const std::string& header,
+                                           const std::string& value)
+  {
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    headers_.push_back(header + ": " + value + "\r\n");
+  }
+
+  void HttpOutput::StateMachine::ClearHeaders()
+  {
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    headers_.clear();
+  }
+
+  void HttpOutput::StateMachine::SendBody(const void* buffer, size_t length)
   {
-    SendOkHeader(contentType.c_str(), true, size, NULL);
-    Send(buffer, size);
+    if (state_ == State_Done)
+    {
+      if (length == 0)
+      {
+        return;
+      }
+      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);
+      }
+    }
+
+    if (state_ == State_WritingHeader)
+    {
+      // Send the HTTP header before writing the body
+
+      stream_.OnHttpStatusReceived(status_);
+
+      std::string s = "HTTP/1.1 " + 
+        boost::lexical_cast<std::string>(status_) +
+        " " + std::string(EnumerationToString(status_)) +
+        "\r\n";
+
+      if (keepAlive_)
+      {
+        s += "Connection: keep-alive\r\n";
+      }
+
+      for (std::list<std::string>::const_iterator
+             it = headers_.begin(); it != headers_.end(); ++it)
+      {
+        s += *it;
+      }
+
+      if (status_ != HttpStatus_200_Ok)
+      {
+        hasContentLength_ = false;
+      }
+
+      uint64_t contentLength = (hasContentLength_ ? contentLength_ : length);
+      s += "Content-Length: " + boost::lexical_cast<std::string>(contentLength) + "\r\n\r\n";
+
+      stream_.Send(true, s.c_str(), s.size());
+      state_ = State_WritingBody;
+    }
+
+    if (hasContentLength_ &&
+        contentPosition_ + length > contentLength_)
+    {
+      LOG(ERROR) << "The body size exceeds what was declared with SetContentSize()";
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (length > 0)
+    {
+      stream_.Send(false, buffer, length);
+      contentPosition_ += length;
+    }
+
+    if (!hasContentLength_ ||
+        contentPosition_ == contentLength_)
+    {
+      state_ = State_Done;
+    }
   }
 
 
-  void HttpOutput::AnswerBufferWithContentType(const void* buffer,
-                                               size_t size,
-                                               const std::string& contentType,
-                                               const HttpHandler::Arguments& cookies)
+  void HttpOutput::SendMethodNotAllowed(const std::string& allowed)
   {
-    Header header;
-    PrepareOkHeader(header, contentType.c_str(), true, size, NULL);
-    PrepareCookies(header, cookies);
-    SendOkHeader(header);
-    Send(buffer, size);
+    stateMachine_.ClearHeaders();
+    stateMachine_.SetHttpStatus(HttpStatus_405_MethodNotAllowed);
+    stateMachine_.AddHeader("Allow", allowed);
+    stateMachine_.SendBody(NULL, 0);
   }
 
 
+  void HttpOutput::SendStatus(HttpStatus status)
+  {
+    if (status == HttpStatus_200_Ok ||
+        status == HttpStatus_301_MovedPermanently ||
+        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);
+    }
+    
+    stateMachine_.ClearHeaders();
+    stateMachine_.SetHttpStatus(status);
+    stateMachine_.SendBody(NULL, 0);
+  }
+
 
   void HttpOutput::Redirect(const std::string& path)
   {
-    std::string s = 
-      "HTTP/1.1 301 " + std::string(EnumerationToString(HttpStatus_301_MovedPermanently)) + 
-      "\r\nLocation: " + path +
-      "\r\n\r\n";
-    Send(&s[0], s.size());  
+    stateMachine_.ClearHeaders();
+    stateMachine_.SetHttpStatus(HttpStatus_301_MovedPermanently);
+    stateMachine_.AddHeader("Location", path);
+    stateMachine_.SendBody(NULL, 0);
+  }
+
+
+  void HttpOutput::SendUnauthorized(const std::string& realm)
+  {
+    stateMachine_.ClearHeaders();
+    stateMachine_.SetHttpStatus(HttpStatus_401_Unauthorized);
+    stateMachine_.AddHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
+    stateMachine_.SendBody(NULL, 0);
+  }
+
+  void HttpOutput::SendBody(const void* buffer, size_t length)
+  {
+    stateMachine_.SendBody(buffer, length);
+  }
+
+  void HttpOutput::SendBody(const std::string& str)
+  {
+    if (str.size() == 0)
+    {
+      stateMachine_.SendBody(NULL, 0);
+    }
+    else
+    {
+      stateMachine_.SendBody(str.c_str(), str.size());
+    }
+  }
+
+  void HttpOutput::SendBody()
+  {
+    stateMachine_.SendBody(NULL, 0);
   }
 }
--- a/Core/HttpServer/HttpOutput.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/HttpOutput.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,64 +36,109 @@
 #include <string>
 #include <stdint.h>
 #include "../Enumerations.h"
+#include "IHttpOutputStream.h"
 #include "HttpHandler.h"
 
 namespace Orthanc
 {
-  class HttpOutput
+  class HttpOutput : public boost::noncopyable
   {
   private:
     typedef std::list< std::pair<std::string, std::string> >  Header;
 
-    void SendHeaderInternal(HttpStatus status);
+    class StateMachine : public boost::noncopyable
+    {
+    private:
+      enum State
+      {
+        State_WritingHeader,      
+        State_WritingBody,
+        State_Done
+      };
+
+      IHttpOutputStream& stream_;
+      State state_;
+
+      HttpStatus status_;
+      bool hasContentLength_;
+      uint64_t contentLength_;
+      uint64_t contentPosition_;
+      bool keepAlive_;
+      std::list<std::string> headers_;
 
-    void PrepareOkHeader(Header& header,
-                         const char* contentType,
-                         bool hasContentLength,
-                         uint64_t contentLength,
-                         const char* contentFilename);
+    public:
+      StateMachine(IHttpOutputStream& stream,
+                   bool isKeepAlive);
+
+      ~StateMachine();
+
+      void SetHttpStatus(HttpStatus status);
+
+      void SetContentLength(uint64_t length);
+
+      void SetContentType(const char* contentType);
 
-    void SendOkHeader(const Header& header);
+      void SetContentFilename(const char* filename);
+
+      void SetCookie(const std::string& cookie,
+                     const std::string& value);
 
-    void PrepareCookies(Header& header,
-                        const HttpHandler::Arguments& cookies);
+      void AddHeader(const std::string& header,
+                     const std::string& value);
+
+      void ClearHeaders();
+
+      void SendBody(const void* buffer, size_t length);
+    };
+
+    StateMachine stateMachine_;
 
   public:
-    virtual ~HttpOutput()
+    HttpOutput(IHttpOutputStream& stream,
+               bool isKeepAlive) : 
+      stateMachine_(stream, isKeepAlive)
     {
     }
 
-    virtual void Send(const void* buffer, size_t length) = 0;
+    void SendStatus(HttpStatus status);
+
+    void SetContentType(const char* contentType)
+    {
+      stateMachine_.SetContentType(contentType);
+    }
+
+    void SetContentFilename(const char* filename)
+    {
+      stateMachine_.SetContentFilename(filename);
+    }
+
+    void SetContentLength(uint64_t length)
+    {
+      stateMachine_.SetContentLength(length);
+    }
 
-    void SendOkHeader(const char* contentType,
-                      bool hasContentLength,
-                      uint64_t contentLength,
-                      const char* contentFilename);
+    void SetCookie(const std::string& cookie,
+                   const std::string& value)
+    {
+      stateMachine_.SetCookie(cookie, value);
+    }
 
-    void SendString(const std::string& s);
+    void AddHeader(const std::string& key,
+                   const std::string& value)
+    {
+      stateMachine_.AddHeader(key, value);
+    }
 
-    void SendMethodNotAllowedError(const std::string& allowed);
+    void SendBody(const void* buffer, size_t length);
+
+    void SendBody(const std::string& str);
 
-    void SendHeader(HttpStatus status);
+    void SendBody();
+
+    void SendMethodNotAllowed(const std::string& allowed);
 
     void Redirect(const std::string& path);
 
-    // Higher-level constructs to send entire buffers ----------------------------
-
-    void AnswerBufferWithContentType(const std::string& buffer,
-                                     const std::string& contentType);
-
-    void AnswerBufferWithContentType(const std::string& buffer,
-                                     const std::string& contentType,
-                                     const HttpHandler::Arguments& cookies);
-
-    void AnswerBufferWithContentType(const void* buffer,
-                                     size_t size,
-                                     const std::string& contentType);
-
-    void AnswerBufferWithContentType(const void* buffer,
-                                     size_t size,
-                                     const std::string& contentType,
-                                     const HttpHandler::Arguments& cookies);
+    void SendUnauthorized(const std::string& realm);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/HttpServer/IHttpOutputStream.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,53 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "../Enumerations.h"
+
+#include <string>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IHttpOutputStream : public boost::noncopyable
+  {
+  public:
+    virtual ~IHttpOutputStream()
+    {
+    }
+
+    virtual void OnHttpStatusReceived(HttpStatus status) = 0;
+
+    virtual void Send(bool isHeader, const void* buffer, size_t length) = 0;
+  };
+}
--- a/Core/HttpServer/MongooseServer.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/MongooseServer.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -68,23 +68,28 @@
   namespace
   {
     // Anonymous namespace to avoid clashes between compilation modules
-    class MongooseOutput : public HttpOutput
+    class MongooseOutputStream : public IHttpOutputStream
     {
     private:
       struct mg_connection* connection_;
 
     public:
-      MongooseOutput(struct mg_connection* connection) : connection_(connection)
+      MongooseOutputStream(struct mg_connection* connection) : connection_(connection)
       {
       }
 
-      virtual void Send(const void* buffer, size_t length)
+      virtual void Send(bool isHeader, const void* buffer, size_t length)
       {
         if (length > 0)
         {
           mg_write(connection_, buffer, length);
         }
       }
+
+      virtual void OnHttpStatusReceived(HttpStatus status)
+      {
+        // Ignore this
+      }
     };
 
 
@@ -255,23 +260,6 @@
 
 
 
-  HttpHandler* MongooseServer::FindHandler(const UriComponents& forUri) const
-  {
-    for (Handlers::const_iterator it = 
-           handlers_.begin(); it != handlers_.end(); ++it) 
-    {
-      if ((*it)->IsServedUri(forUri))
-      {
-        return *it;
-      }
-    }
-
-    return NULL;
-  }
-
-
-
-
   static PostDataStatus ReadBody(std::string& postData,
                                  struct mg_connection *connection,
                                  const HttpHandler::Arguments& headers)
@@ -421,18 +409,8 @@
   }
 
 
-  static void SendUnauthorized(HttpOutput& output)
-  {
-    std::string s = "HTTP/1.1 401 Unauthorized\r\n" 
-      "WWW-Authenticate: Basic realm=\"" ORTHANC_REALM "\""
-      "\r\n\r\n";
-    output.Send(&s[0], s.size());
-  }
-
-
-  static bool Authorize(const MongooseServer& that,
-                        const HttpHandler::Arguments& headers,
-                        HttpOutput& output)
+  static bool IsAccessGranted(const MongooseServer& that,
+                              const HttpHandler::Arguments& headers)
   {
     bool granted = false;
 
@@ -440,22 +418,15 @@
     if (auth != headers.end())
     {
       std::string s = auth->second;
-      if (s.substr(0, 6) == "Basic ")
+      if (s.size() > 6 &&
+          s.substr(0, 6) == "Basic ")
       {
         std::string b64 = s.substr(6);
         granted = that.IsValidBasicHttpAuthentication(b64);
       }
     }
 
-    if (!granted)
-    {
-      SendUnauthorized(output);
-      return false;
-    }
-    else
-    {
-      return true;
-    }
+    return granted;
   }
 
 
@@ -469,7 +440,8 @@
     }
 
     std::string s = auth->second;
-    if (s.substr(0, 6) != "Basic ")
+    if (s.size() <= 6 ||
+        s.substr(0, 6) != "Basic ")
     {
       return "";
     }
@@ -494,7 +466,7 @@
   static bool ExtractMethod(HttpMethod& method,
                             const struct mg_request_info *request,
                             const HttpHandler::Arguments& headers,
-                            const HttpHandler::Arguments& argumentsGET)
+                            const HttpHandler::GetArguments& argumentsGET)
   {
     std::string overriden;
 
@@ -512,10 +484,13 @@
     {
       // 2. Faking with Ruby on Rail's approach
       // GET /my/resource?_method=delete <=> DELETE /my/resource
-      methodOverride = argumentsGET.find("_method");
-      if (methodOverride != argumentsGET.end())
+      for (size_t i = 0; i < argumentsGET.size(); i++)
       {
-        overriden = methodOverride->second;
+        if (argumentsGET[i].first == "_method")
+        {
+          overriden = argumentsGET[i].second;
+          break;
+        }
       }
     }
 
@@ -568,179 +543,229 @@
   }
 
 
+  static void InternalCallback(struct mg_connection *connection,
+                               const struct mg_request_info *request)
+  {
+    MongooseServer* that = reinterpret_cast<MongooseServer*>(request->user_data);
+    MongooseOutputStream stream(connection);
+    HttpOutput output(stream, that->IsKeepAliveEnabled());
 
+    // Check remote calls
+    if (!that->IsRemoteAccessAllowed() &&
+        request->remote_ip != LOCALHOST)
+    {
+      output.SendUnauthorized(ORTHANC_REALM);
+      return;
+    }
+
+
+    // Extract the HTTP headers
+    HttpHandler::Arguments headers;
+    for (int i = 0; i < request->num_headers; i++)
+    {
+      std::string name = request->http_headers[i].name;
+      std::transform(name.begin(), name.end(), name.begin(), ::tolower);
+      headers.insert(std::make_pair(name, request->http_headers[i].value));
+    }
+
+
+    // Extract the GET arguments
+    HttpHandler::GetArguments argumentsGET;
+    if (!strcmp(request->request_method, "GET"))
+    {
+      HttpHandler::ParseGetArguments(argumentsGET, request->query_string);
+    }
+
+
+    // Compute the HTTP method, taking method faking into consideration
+    HttpMethod method = HttpMethod_Get;
+    if (!ExtractMethod(method, request, headers, argumentsGET))
+    {
+      output.SendStatus(HttpStatus_400_BadRequest);
+      return;
+    }
+
+
+    // Authenticate this connection
+    if (that->IsAuthenticationEnabled() && !IsAccessGranted(*that, headers))
+    {
+      output.SendUnauthorized(ORTHANC_REALM);
+      return;
+    }
+
+
+    // Apply the filter, if it is installed
+    const IIncomingHttpRequestFilter *filter = that->GetIncomingHttpRequestFilter();
+    if (filter != NULL)
+    {
+      std::string username = GetAuthenticatedUsername(headers);
+
+      char remoteIp[24];
+      sprintf(remoteIp, "%d.%d.%d.%d", 
+              reinterpret_cast<const uint8_t*>(&request->remote_ip) [3], 
+              reinterpret_cast<const uint8_t*>(&request->remote_ip) [2], 
+              reinterpret_cast<const uint8_t*>(&request->remote_ip) [1], 
+              reinterpret_cast<const uint8_t*>(&request->remote_ip) [0]);
+
+      if (!filter->IsAllowed(method, request->uri, remoteIp, username.c_str()))
+      {
+        output.SendUnauthorized(ORTHANC_REALM);
+        return;
+      }
+    }
+
+
+    // Extract the body of the request for PUT and POST
+    std::string body;
+    if (method == HttpMethod_Post ||
+        method == HttpMethod_Put)
+    {
+      PostDataStatus status;
+
+      HttpHandler::Arguments::const_iterator ct = headers.find("content-type");
+      if (ct == headers.end())
+      {
+        // No content-type specified. Assume no multi-part content occurs at this point.
+        status = ReadBody(body, connection, headers);          
+      }
+      else
+      {
+        std::string contentType = ct->second;
+        if (contentType.size() >= multipartLength &&
+            !memcmp(contentType.c_str(), multipart, multipartLength))
+        {
+          status = ParseMultipartPost(body, connection, headers, contentType, that->GetChunkStore());
+        }
+        else
+        {
+          status = ReadBody(body, connection, headers);
+        }
+      }
+
+      switch (status)
+      {
+        case PostDataStatus_NoLength:
+          output.SendStatus(HttpStatus_411_LengthRequired);
+          return;
+
+        case PostDataStatus_Failure:
+          output.SendStatus(HttpStatus_400_BadRequest);
+          return;
+
+        case PostDataStatus_Pending:
+          output.SendBody();
+          return;
+
+        default:
+          break;
+      }
+    }
+
+
+    // Decompose the URI into its components
+    UriComponents uri;
+    try
+    {
+      Toolbox::SplitUriComponents(uri, request->uri);
+    }
+    catch (OrthancException)
+    {
+      output.SendStatus(HttpStatus_400_BadRequest);
+      return;
+    }
+
+
+    // Loop over the candidate handlers for this URI
+    LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri);
+    bool found = false;
+
+    for (MongooseServer::Handlers::const_iterator it = 
+           that->GetHandlers().begin(); it != that->GetHandlers().end() && !found; ++it) 
+    {
+      try
+      {
+        found = (*it)->Handle(output, method, uri, headers, argumentsGET, body);
+      }
+      catch (OrthancException& e)
+      {
+        // Using this candidate handler results in an exception
+        LOG(ERROR) << "Exception in the HTTP handler: " << e.What();
+
+        switch (e.GetErrorCode())
+        {
+          case ErrorCode_InexistentFile:
+          case ErrorCode_InexistentItem:
+          case ErrorCode_UnknownResource:
+            output.SendStatus(HttpStatus_404_NotFound);
+            break;
+
+          case ErrorCode_BadRequest:
+          case ErrorCode_UriSyntax:
+            output.SendStatus(HttpStatus_400_BadRequest);
+            break;
+
+          default:
+            output.SendStatus(HttpStatus_500_InternalServerError);
+        }
+
+        return;
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        LOG(ERROR) << "Exception in the HTTP handler: Bad lexical cast";
+        output.SendStatus(HttpStatus_400_BadRequest);
+        return;
+      }
+      catch (std::runtime_error&)
+      {
+        LOG(ERROR) << "Exception in the HTTP handler: Presumably a bad JSON request";
+        output.SendStatus(HttpStatus_400_BadRequest);
+        return;
+      }
+    }
+
+    if (!found)
+    {
+      output.SendStatus(HttpStatus_404_NotFound);
+    }
+  }
+
+
+#if MONGOOSE_USE_CALLBACKS == 0
   static void* Callback(enum mg_event event,
                         struct mg_connection *connection,
                         const struct mg_request_info *request)
   {
     if (event == MG_NEW_REQUEST) 
     {
-      MongooseServer* that = reinterpret_cast<MongooseServer*>(request->user_data);
-      MongooseOutput output(connection);
-
-      // Check remote calls
-      if (!that->IsRemoteAccessAllowed() &&
-          request->remote_ip != LOCALHOST)
-      {
-        SendUnauthorized(output);
-        return (void*) "";
-      }
-
-
-      // Extract the HTTP headers
-      HttpHandler::Arguments headers;
-      for (int i = 0; i < request->num_headers; i++)
-      {
-        std::string name = request->http_headers[i].name;
-        std::transform(name.begin(), name.end(), name.begin(), ::tolower);
-        headers.insert(std::make_pair(name, request->http_headers[i].value));
-      }
-
-
-      // Extract the GET arguments
-      HttpHandler::Arguments argumentsGET;
-      if (!strcmp(request->request_method, "GET"))
-      {
-        HttpHandler::ParseGetQuery(argumentsGET, request->query_string);
-      }
-
-
-      // Compute the HTTP method, taking method faking into consideration
-      HttpMethod method;
-      if (!ExtractMethod(method, request, headers, argumentsGET))
-      {
-        output.SendHeader(HttpStatus_400_BadRequest);
-        return (void*) "";
-      }
-
-
-      // Authenticate this connection
-      if (that->IsAuthenticationEnabled() &&
-          !Authorize(*that, headers, output))
-      {
-        return (void*) "";
-      }
-
-
-      // Apply the filter, if it is installed
-      const IIncomingHttpRequestFilter *filter = that->GetIncomingHttpRequestFilter();
-      if (filter != NULL)
-      {
-        std::string username = GetAuthenticatedUsername(headers);
-
-        char remoteIp[24];
-        sprintf(remoteIp, "%d.%d.%d.%d", 
-                reinterpret_cast<const uint8_t*>(&request->remote_ip) [3], 
-                reinterpret_cast<const uint8_t*>(&request->remote_ip) [2], 
-                reinterpret_cast<const uint8_t*>(&request->remote_ip) [1], 
-                reinterpret_cast<const uint8_t*>(&request->remote_ip) [0]);
-
-        if (!filter->IsAllowed(method, request->uri, remoteIp, username.c_str()))
-        {
-          SendUnauthorized(output);
-          return (void*) "";
-        }
-      }
-
-
-      // Extract the body of the request for PUT and POST
-      std::string body;
-      if (method == HttpMethod_Post ||
-          method == HttpMethod_Put)
-      {
-        PostDataStatus status;
-
-        HttpHandler::Arguments::const_iterator ct = headers.find("content-type");
-        if (ct == headers.end())
-        {
-          // No content-type specified. Assume no multi-part content occurs at this point.
-          status = ReadBody(body, connection, headers);          
-        }
-        else
-        {
-          std::string contentType = ct->second;
-          if (contentType.size() >= multipartLength &&
-              !memcmp(contentType.c_str(), multipart, multipartLength))
-          {
-            status = ParseMultipartPost(body, connection, headers, contentType, that->GetChunkStore());
-          }
-          else
-          {
-            status = ReadBody(body, connection, headers);
-          }
-        }
-
-        switch (status)
-        {
-          case PostDataStatus_NoLength:
-            output.SendHeader(HttpStatus_411_LengthRequired);
-            return (void*) "";
-
-          case PostDataStatus_Failure:
-            output.SendHeader(HttpStatus_400_BadRequest);
-            return (void*) "";
-
-          case PostDataStatus_Pending:
-            output.AnswerBufferWithContentType(NULL, 0, "");
-            return (void*) "";
-
-          default:
-            break;
-        }
-      }
-
-
-      // Call the proper handler for this URI
-      UriComponents uri;
-      try
-      {
-        Toolbox::SplitUriComponents(uri, request->uri);
-      }
-      catch (OrthancException)
-      {
-        output.SendHeader(HttpStatus_400_BadRequest);
-        return (void*) "";
-      }
-
-
-      HttpHandler* handler = that->FindHandler(uri);
-      if (handler)
-      {
-        try
-        {
-          LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri);
-          handler->Handle(output, method, uri, headers, argumentsGET, body);
-        }
-        catch (OrthancException& e)
-        {
-          LOG(ERROR) << "MongooseServer Exception [" << e.What() << "]";
-          output.SendHeader(HttpStatus_500_InternalServerError);        
-        }
-        catch (boost::bad_lexical_cast&)
-        {
-          LOG(ERROR) << "MongooseServer Exception: Bad lexical cast";
-          output.SendHeader(HttpStatus_400_BadRequest);
-        }
-        catch (std::runtime_error&)
-        {
-          LOG(ERROR) << "MongooseServer Exception: Presumably a bad JSON request";
-          output.SendHeader(HttpStatus_400_BadRequest);
-        }
-      }
-      else
-      {
-        output.SendHeader(HttpStatus_404_NotFound);
-      }
+      InternalCallback(connection, request);
 
       // Mark as processed
       return (void*) "";
-    } 
-    else 
+    }
+    else
     {
       return NULL;
     }
   }
 
+#elif MONGOOSE_USE_CALLBACKS == 1
+  static int Callback(struct mg_connection *connection)
+  {
+    struct mg_request_info *request = mg_get_request_info(connection);
+
+    InternalCallback(connection, request);
+
+    return 1;  // Do not let Mongoose handle the request by itself
+  }
+
+#else
+#error Please set MONGOOSE_USE_CALLBACKS
+#endif
+
+
+
+
 
   bool MongooseServer::IsRunning() const
   {
@@ -756,6 +781,7 @@
     ssl_ = false;
     port_ = 8000;
     filter_ = NULL;
+    keepAlive_ = false;
 
 #if ORTHANC_SSL_ENABLED == 1
     // Check for the Heartbleed exploit
@@ -794,13 +820,32 @@
       }
 
       const char *options[] = {
+        // Set the TCP port for the HTTP server
         "listening_ports", port.c_str(), 
+        
+        // Optimization reported by Chris Hafey
+        // https://groups.google.com/d/msg/orthanc-users/CKueKX0pJ9E/_UCbl8T-VjIJ
+        "enable_keep_alive", (keepAlive_ ? "yes" : "no"),
+
+        // Set the SSL certificate, if any. This must be the last option.
         ssl_ ? "ssl_certificate" : NULL,
         certificate_.c_str(),
         NULL
       };
 
+#if MONGOOSE_USE_CALLBACKS == 0
       pimpl_->context_ = mg_start(&Callback, this, options);
+
+#elif MONGOOSE_USE_CALLBACKS == 1
+      struct mg_callbacks callbacks;
+      memset(&callbacks, 0, sizeof(callbacks));
+      callbacks.begin_request = Callback;
+      pimpl_->context_ = mg_start(&callbacks, this, options);
+
+#else
+#error Please set MONGOOSE_USE_CALLBACKS
+#endif
+
       if (!pimpl_->context_)
       {
         throw OrthancException("Unable to launch the Mongoose server");
@@ -818,23 +863,17 @@
   }
 
 
-  void MongooseServer::RegisterHandler(HttpHandler* handler)
+  void MongooseServer::RegisterHandler(HttpHandler& handler)
   {
     Stop();
 
-    handlers_.push_back(handler);
+    handlers_.push_back(&handler);
   }
 
 
   void MongooseServer::ClearHandlers()
   {
     Stop();
-
-    for (Handlers::iterator it = 
-           handlers_.begin(); it != handlers_.end(); ++it)
-    {
-      delete *it;
-    }
   }
 
 
@@ -874,6 +913,15 @@
 #endif
   }
 
+
+  void MongooseServer::SetKeepAliveEnabled(bool enabled)
+  {
+    Stop();
+    keepAlive_ = enabled;
+    LOG(WARNING) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled");
+  }
+
+
   void MongooseServer::SetAuthenticationEnabled(bool enabled)
   {
     Stop();
--- a/Core/HttpServer/MongooseServer.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/HttpServer/MongooseServer.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -59,12 +59,14 @@
 
   class MongooseServer
   {
+  public:
+    typedef std::list<HttpHandler*> Handlers;
+
   private:
     // http://stackoverflow.com/questions/311166/stdauto-ptr-or-boostshared-ptr-for-pimpl-idiom
     struct PImpl;
     boost::shared_ptr<PImpl> pimpl_;
 
-    typedef std::list<HttpHandler*> Handlers;
     Handlers handlers_;
 
     typedef std::set<std::string> RegisteredUsers;
@@ -76,6 +78,7 @@
     std::string certificate_;
     uint16_t port_;
     IIncomingHttpRequestFilter* filter_;
+    bool keepAlive_;
   
     bool IsRunning() const;
 
@@ -100,7 +103,7 @@
     void RegisterUser(const char* username,
                       const char* password);
 
-    void RegisterHandler(HttpHandler* handler);  // This takes the ownership
+    void RegisterHandler(HttpHandler& handler);
 
     bool IsAuthenticationEnabled() const
     {
@@ -116,6 +119,13 @@
 
     void SetSslEnabled(bool enabled);
 
+    bool IsKeepAliveEnabled() const
+    {
+      return keepAlive_;
+    }
+
+    void SetKeepAliveEnabled(bool enabled);
+
     const std::string& GetSslCertificate() const
     {
       return certificate_;
@@ -139,11 +149,13 @@
 
     void ClearHandlers();
 
-    // Can return NULL if no handler is associated to this URI
-    HttpHandler* FindHandler(const UriComponents& forUri) const;
-
     ChunkStore& GetChunkStore();
 
     bool IsValidBasicHttpAuthentication(const std::string& basic) const;
+
+    const Handlers& GetHandlers() const
+    {
+      return handlers_;
+    }
   };
 }
--- a/Core/ICommand.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ICommand.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/IDynamicObject.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/IDynamicObject.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/ImageFormats/ImageAccessor.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ImageFormats/ImageAccessor.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -168,7 +168,7 @@
     pitch_ = pitch;
     buffer_ = const_cast<void*>(buffer);
 
-    assert(GetBytesPerPixel(format_) * width_ <= pitch_);
+    assert(GetBytesPerPixel() * width_ <= pitch_);
   }
 
 
@@ -185,7 +185,7 @@
     pitch_ = pitch;
     buffer_ = buffer;
 
-    assert(GetBytesPerPixel(format_) * width_ <= pitch_);
+    assert(GetBytesPerPixel() * width_ <= pitch_);
   }
 
 
--- a/Core/ImageFormats/ImageAccessor.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ImageFormats/ImageAccessor.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -62,6 +62,11 @@
       return format_;
     }
 
+    unsigned int GetBytesPerPixel() const
+    {
+      return ::Orthanc::GetBytesPerPixel(format_);
+    }
+
     unsigned int GetWidth() const
     {
       return width_;
--- a/Core/ImageFormats/ImageBuffer.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ImageFormats/ImageBuffer.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/ImageFormats/ImageBuffer.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ImageFormats/ImageBuffer.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/ImageFormats/ImageProcessing.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ImageFormats/ImageProcessing.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -75,6 +75,44 @@
   }
 
 
+  template <typename TargetType>
+  static void ConvertColorToGrayscale(ImageAccessor& target,
+                              const ImageAccessor& source)
+  {
+    assert(source.GetFormat() == PixelFormat_RGB24);
+
+    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++)
+    {
+      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)
+      {
+        // Y = 0.2126 R + 0.7152 G + 0.0722 B
+        int32_t v = (2126 * static_cast<int32_t>(s[0]) +
+                     7152 * static_cast<int32_t>(s[1]) +
+                     0722 * static_cast<int32_t>(s[2])) / 1000;
+        
+        if (static_cast<int32_t>(v) < static_cast<int32_t>(minValue))
+        {
+          *t = minValue;
+        }
+        else if (static_cast<int32_t>(v) > static_cast<int32_t>(maxValue))
+        {
+          *t = maxValue;
+        }
+        else
+        {
+          *t = static_cast<TargetType>(v);
+        }
+      }
+    }
+  }
+
+
   template <typename PixelType>
   static void SetInternal(ImageAccessor& image,
                           int64_t constant)
@@ -171,7 +209,7 @@
   void MultiplyConstantInternal(ImageAccessor& image,
                                 float factor)
   {
-    if (abs(factor - 1.0f) <= std::numeric_limits<float>::epsilon())
+    if (std::abs(factor - 1.0f) <= std::numeric_limits<float>::epsilon())
     {
       return;
     }
@@ -319,6 +357,27 @@
       return;
     }
 
+    if (target.GetFormat() == PixelFormat_Grayscale8 &&
+        source.GetFormat() == PixelFormat_RGB24)
+    {
+      ConvertColorToGrayscale<uint8_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale16 &&
+        source.GetFormat() == PixelFormat_RGB24)
+    {
+      ConvertColorToGrayscale<uint16_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_SignedGrayscale16 &&
+        source.GetFormat() == PixelFormat_RGB24)
+    {
+      ConvertColorToGrayscale<int16_t>(target, source);
+      return;
+    }
+
     throw OrthancException(ErrorCode_NotImplemented);
   }
 
--- a/Core/ImageFormats/ImageProcessing.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ImageFormats/ImageProcessing.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/ImageFormats/PngReader.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ImageFormats/PngReader.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/ImageFormats/PngReader.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ImageFormats/PngReader.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/ImageFormats/PngWriter.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ImageFormats/PngWriter.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/ImageFormats/PngWriter.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/ImageFormats/PngWriter.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Lua/LuaContext.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Lua/LuaContext.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,6 +34,7 @@
 #include "LuaContext.h"
 
 #include <glog/logging.h>
+#include <cassert>
 
 extern "C" 
 {
@@ -43,7 +44,7 @@
 
 namespace Orthanc
 {
-  int LuaContext::PrintToLog(lua_State *state)
+  LuaContext& LuaContext::GetLuaContext(lua_State *state)
   {
     // Get the pointer to the "LuaContext" underlying object
     lua_getglobal(state, "_LuaContext");
@@ -52,6 +53,13 @@
     assert(that != NULL);
     lua_pop(state, 1);
 
+    return *that;
+  }
+
+  int LuaContext::PrintToLog(lua_State *state)
+  {
+    LuaContext& that = GetLuaContext(state);
+
     // http://medek.wordpress.com/2009/02/03/wrapping-lua-errors-and-print-function/
     int nArgs = lua_gettop(state);
     lua_getglobal(state, "tostring");
@@ -78,14 +86,243 @@
       lua_pop(state, 1);
     }
 
-    LOG(INFO) << "Lua says: " << result;         
-    that->log_.append(result);
-    that->log_.append("\n");
+    LOG(WARNING) << "Lua says: " << result;         
+    that.log_.append(result);
+    that.log_.append("\n");
+
+    return 0;
+  }
+
+
+  int LuaContext::SetHttpCredentials(lua_State *state)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    if (nArgs != 2 ||
+        !lua_isstring(state, 1) ||  // Username
+        !lua_isstring(state, 2))    // Password
+    {
+      LOG(ERROR) << "Lua: Bad parameters to SetHttpCredentials()";
+    }
+    else
+    {
+      // Configure the HTTP client
+      const char* username = lua_tostring(state, 1);
+      const char* password = lua_tostring(state, 2);
+      that.httpClient_.SetCredentials(username, password);
+    }
 
     return 0;
   }
 
 
+  bool LuaContext::AnswerHttpQuery(lua_State* state)
+  {
+    std::string str;
+
+    try
+    {
+      httpClient_.Apply(str);
+    }
+    catch (OrthancException& e)
+    {
+      return false;
+    }
+
+    // Return the result of the HTTP request
+    lua_pushstring(state, str.c_str());
+
+    return true;
+  }
+
+  
+  int LuaContext::CallHttpGet(lua_State *state)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    if (nArgs != 1 || !lua_isstring(state, 1))  // URL
+    {
+      LOG(ERROR) << "Lua: Bad parameters to HttpGet()";
+      lua_pushstring(state, "ERROR");
+      return 1;
+    }
+
+    // Configure the HTTP client class
+    const char* url = lua_tostring(state, 1);
+    that.httpClient_.SetMethod(HttpMethod_Get);
+    that.httpClient_.SetUrl(url);
+
+    // Do the HTTP GET request
+    if (!that.AnswerHttpQuery(state))
+    {
+      LOG(ERROR) << "Lua: Error in HttpGet() for URL " << url;
+      lua_pushstring(state, "ERROR");
+    }
+
+    return 1;
+  }
+
+
+  int LuaContext::CallHttpPostOrPut(lua_State *state,
+                                    HttpMethod method)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    if ((nArgs != 1 && nArgs != 2) ||
+        !lua_isstring(state, 1) ||                // URL
+        (nArgs >= 2 && !lua_isstring(state, 2)))  // Body data
+    {
+      LOG(ERROR) << "Lua: Bad parameters to HttpPost() or HttpPut()";
+      lua_pushstring(state, "ERROR");
+      return 1;
+    }
+
+    // Configure the HTTP client class
+    const char* url = lua_tostring(state, 1);
+    that.httpClient_.SetMethod(method);
+    that.httpClient_.SetUrl(url);
+
+    if (nArgs >= 2)
+    {
+      that.httpClient_.SetPostData(lua_tostring(state, 2));
+    }
+    else
+    {
+      that.httpClient_.AccessPostData().clear();
+    }
+
+    // Do the HTTP POST/PUT request
+    if (!that.AnswerHttpQuery(state))
+    {
+      LOG(ERROR) << "Lua: Error in HttpPost() or HttpPut() for URL " << url;
+      lua_pushstring(state, "ERROR");
+    }
+
+    return 1;
+  }
+
+
+  int LuaContext::CallHttpPost(lua_State *state)
+  {
+    return CallHttpPostOrPut(state, HttpMethod_Post);
+  }
+
+
+  int LuaContext::CallHttpPut(lua_State *state)
+  {
+    return CallHttpPostOrPut(state, HttpMethod_Put);
+  }
+
+
+  int LuaContext::CallHttpDelete(lua_State *state)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    if (nArgs != 1 || !lua_isstring(state, 1))  // URL
+    {
+      LOG(ERROR) << "Lua: Bad parameters to HttpDelete()";
+      lua_pushstring(state, "ERROR");
+      return 1;
+    }
+
+    // Configure the HTTP client class
+    const char* url = lua_tostring(state, 1);
+    that.httpClient_.SetMethod(HttpMethod_Delete);
+    that.httpClient_.SetUrl(url);
+
+    // Do the HTTP DELETE request
+    std::string s;
+    if (!that.httpClient_.Apply(s))
+    {
+      LOG(ERROR) << "Lua: Error in HttpDelete() for URL " << url;
+      lua_pushstring(state, "ERROR");
+    }
+    else
+    {
+      lua_pushstring(state, "SUCCESS");
+    }
+
+    return 1;
+  }
+
+
+  void LuaContext::PushJson(const Json::Value& value)
+  {
+    if (value.isString())
+    {
+      lua_pushstring(lua_, value.asCString());
+    }
+    else if (value.isDouble())
+    {
+      lua_pushnumber(lua_, value.asDouble());
+    }
+    else if (value.isInt())
+    {
+      lua_pushinteger(lua_, value.asInt());
+    }
+    else if (value.isUInt())
+    {
+      lua_pushinteger(lua_, value.asUInt());
+    }
+    else if (value.isBool())
+    {
+      lua_pushboolean(lua_, value.asBool());
+    }
+    else if (value.isNull())
+    {
+      lua_pushnil(lua_);
+    }
+    else if (value.isArray())
+    {
+      lua_newtable(lua_);
+
+      // http://lua-users.org/wiki/SimpleLuaApiExample
+      for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
+      {
+        // Push the table index (note the "+1" because of Lua conventions)
+        lua_pushnumber(lua_, i + 1);
+
+        // Push the value of the cell
+        PushJson(value[i]);
+
+        // Stores the pair in the table
+        lua_rawset(lua_, -3);
+      }
+    }
+    else if (value.isObject())
+    {
+      lua_newtable(lua_);
+
+      Json::Value::Members members = value.getMemberNames();
+
+      for (Json::Value::Members::const_iterator 
+             it = members.begin(); it != members.end(); ++it)
+      {
+        // Push the index of the cell
+        lua_pushstring(lua_, it->c_str());
+
+        // Push the value of the cell
+        PushJson(value[*it]);
+
+        // Stores the pair in the table
+        lua_rawset(lua_, -3);
+      }
+    }
+    else
+    {
+      throw LuaException("Unsupported JSON conversion");
+    }
+  }
+
+
   LuaContext::LuaContext()
   {
     lua_ = luaL_newstate();
@@ -96,6 +333,11 @@
 
     luaL_openlibs(lua_);
     lua_register(lua_, "print", PrintToLog);
+    lua_register(lua_, "HttpGet", CallHttpGet);
+    lua_register(lua_, "HttpPost", CallHttpPost);
+    lua_register(lua_, "HttpPut", CallHttpPut);
+    lua_register(lua_, "HttpDelete", CallHttpDelete);
+    lua_register(lua_, "SetHttpCredentials", SetHttpCredentials);
     
     lua_pushlightuserdata(lua_, this);
     lua_setglobal(lua_, "_LuaContext");
@@ -108,11 +350,9 @@
   }
 
 
-  void LuaContext::Execute(std::string* output,
-                           const std::string& command)
+  void LuaContext::ExecuteInternal(std::string* output,
+                                   const std::string& command)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-
     log_.clear();
     int error = (luaL_loadbuffer(lua_, command.c_str(), command.size(), "line") ||
                  lua_pcall(lua_, 0, 0, 0));
@@ -123,6 +363,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 LuaException(description);
     }
 
@@ -137,15 +378,29 @@
   {
     std::string command;
     EmbeddedResources::GetFileResource(command, resource);
-    Execute(command);
+    ExecuteInternal(NULL, command);
   }
 
 
   bool LuaContext::IsExistingFunction(const char* name)
   {
-    boost::mutex::scoped_lock lock(mutex_);
     lua_settop(lua_, 0);
     lua_getglobal(lua_, name);
     return lua_type(lua_, -1) == LUA_TFUNCTION;
   }
+
+
+  void LuaContext::Execute(Json::Value& output,
+                           const std::string& command)
+  {
+    std::string s;
+    ExecuteInternal(&s, command);
+
+    Json::Reader reader;
+    if (!reader.parse(s, output))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
 }
--- a/Core/Lua/LuaContext.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Lua/LuaContext.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,8 +33,7 @@
 #pragma once
 
 #include "LuaException.h"
-
-#include <boost/thread.hpp>
+#include "../HttpClient.h"
 
 extern "C" 
 {
@@ -42,7 +41,7 @@
 }
 
 #include <EmbeddedResources.h>
-
+#include <boost/noncopyable.hpp>
 
 namespace Orthanc
 {
@@ -52,14 +51,29 @@
     friend class LuaFunctionCall;
 
     lua_State *lua_;
-    boost::mutex mutex_;
     std::string log_;
+    HttpClient httpClient_;
+
+    static LuaContext& GetLuaContext(lua_State *state);
+
+    static int PrintToLog(lua_State *state);
+
+    static int SetHttpCredentials(lua_State *state);
 
-    static int PrintToLog(lua_State *L);
+    static int CallHttpPostOrPut(lua_State *state,
+                                 HttpMethod method);
+    static int CallHttpGet(lua_State *state);
+    static int CallHttpPost(lua_State *state);
+    static int CallHttpPut(lua_State *state);
+    static int CallHttpDelete(lua_State *state);
 
-    void Execute(std::string* output,
-                 const std::string& command);
+    bool AnswerHttpQuery(lua_State* state);
 
+    void ExecuteInternal(std::string* output,
+                         const std::string& command);
+
+    void PushJson(const Json::Value& value);
+    
   public:
     LuaContext();
 
@@ -67,17 +81,31 @@
 
     void Execute(const std::string& command)
     {
-      Execute(NULL, command);
+      ExecuteInternal(NULL, command);
     }
 
     void Execute(std::string& output,
                  const std::string& command)
     {
-      Execute(&output, command);
+      ExecuteInternal(&output, command);
     }
 
+    void Execute(Json::Value& output,
+                 const std::string& command);
+
     void Execute(EmbeddedResources::FileResourceId resource);
 
     bool IsExistingFunction(const char* name);
+
+    void SetHttpCredentials(const char* username,
+                            const char* password)
+    {
+      httpClient_.SetCredentials(username, password);
+    }
+
+    void SetHttpProxy(const std::string& proxy)
+    {
+      httpClient_.SetProxy(proxy);
+    }
   };
 }
--- a/Core/Lua/LuaException.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Lua/LuaException.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Lua/LuaFunctionCall.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Lua/LuaFunctionCall.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,6 +33,10 @@
 #include "../PrecompiledHeaders.h"
 #include "LuaFunctionCall.h"
 
+#include <cassert>
+#include <stdio.h>
+#include <boost/lexical_cast.hpp>
+#include <glog/logging.h>
 
 namespace Orthanc
 {
@@ -47,7 +51,6 @@
   LuaFunctionCall::LuaFunctionCall(LuaContext& context,
                                    const char* functionName) : 
     context_(context),
-    lock_(context.mutex_),
     isExecuted_(false)
   {
     // Clear the stack to fulfill the invariant
@@ -79,77 +82,13 @@
     lua_pushnumber(context_.lua_, value);
   }
 
-  void LuaFunctionCall::PushJSON(const Json::Value& value)
+  void LuaFunctionCall::PushJson(const Json::Value& value)
   {
     CheckAlreadyExecuted();
-
-    if (value.isString())
-    {
-      lua_pushstring(context_.lua_, value.asCString());
-    }
-    else if (value.isDouble())
-    {
-      lua_pushnumber(context_.lua_, value.asDouble());
-    }
-    else if (value.isInt())
-    {
-      lua_pushinteger(context_.lua_, value.asInt());
-    }
-    else if (value.isUInt())
-    {
-      lua_pushinteger(context_.lua_, value.asUInt());
-    }
-    else if (value.isBool())
-    {
-      lua_pushboolean(context_.lua_, value.asBool());
-    }
-    else if (value.isNull())
-    {
-      lua_pushnil(context_.lua_);
-    }
-    else if (value.isArray())
-    {
-      lua_newtable(context_.lua_);
-
-      // http://lua-users.org/wiki/SimpleLuaApiExample
-      for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
-      {
-        // Push the table index (note the "+1" because of Lua conventions)
-        lua_pushnumber(context_.lua_, i + 1);
-
-        // Push the value of the cell
-        PushJSON(value[i]);
-
-        // Stores the pair in the table
-        lua_rawset(context_.lua_, -3);
-      }
-    }
-    else if (value.isObject())
-    {
-      lua_newtable(context_.lua_);
-
-      Json::Value::Members members = value.getMemberNames();
-
-      for (Json::Value::Members::const_iterator 
-             it = members.begin(); it != members.end(); ++it)
-      {
-        // Push the index of the cell
-        lua_pushstring(context_.lua_, it->c_str());
-
-        // Push the value of the cell
-        PushJSON(value[*it]);
-
-        // Stores the pair in the table
-        lua_rawset(context_.lua_, -3);
-      }
-    }
-    else
-    {
-      throw LuaException("Unsupported JSON conversion");
-    }
+    context_.PushJson(value);
   }
 
-  void LuaFunctionCall::Execute(int numOutputs)
+  void LuaFunctionCall::ExecuteInternal(int numOutputs)
   {
     CheckAlreadyExecuted();
 
@@ -176,13 +115,8 @@
 
   bool LuaFunctionCall::ExecutePredicate()
   {
-    Execute(1);
-        
-    if (lua_gettop(context_.lua_) == 0)
-    {
-      throw LuaException("No output was provided by the function");
-    }
-
+    ExecuteInternal(1);
+    
     if (!lua_isboolean(context_.lua_, 1))
     {
       throw LuaException("The function is not a predicate (only true/false outputs allowed)");
@@ -190,4 +124,99 @@
 
     return lua_toboolean(context_.lua_, 1) != 0;
   }
+
+
+  static void PopJson(Json::Value& result,
+                      lua_State* lua,
+                      int top)
+  {
+    if (lua_istable(lua, top))
+    {
+      Json::Value tmp = Json::objectValue;
+      bool isArray = true;
+      size_t size = 0;
+
+      // http://stackoverflow.com/a/6142700/881731
+      
+      // Push another reference to the table on top of the stack (so we know
+      // where it is, and this function can work for negative, positive and
+      // pseudo indices
+      lua_pushvalue(lua, top);
+      // stack now contains: -1 => table
+      lua_pushnil(lua);
+      // stack now contains: -1 => nil; -2 => table
+      while (lua_next(lua, -2))
+      {
+        // stack now contains: -1 => value; -2 => key; -3 => table
+        // copy the key so that lua_tostring does not modify the original
+        lua_pushvalue(lua, -2);
+        // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table
+        std::string key(lua_tostring(lua, -1));
+        Json::Value v;
+        PopJson(v, lua, -2);
+
+        tmp[key] = v;
+
+        size += 1;
+        try
+        {
+          if (boost::lexical_cast<size_t>(key) != size)
+          {
+            isArray = false;
+          }
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+          isArray = false;
+        }
+        
+        // pop value + copy of key, leaving original key
+        lua_pop(lua, 2);
+        // stack now contains: -1 => key; -2 => table
+      }
+      // stack now contains: -1 => table (when lua_next returns 0 it pops the key
+      // but does not push anything.)
+      // Pop table
+      lua_pop(lua, 1);
+
+      // Stack is now the same as it was on entry to this function
+
+      if (isArray)
+      {
+        result = Json::arrayValue;
+        for (size_t i = 0; i < size; i++)
+        {
+          result.append(tmp[boost::lexical_cast<std::string>(i + 1)]);
+        }
+      }
+      else
+      {
+        result = tmp;
+      }
+    }
+    else if (lua_isnumber(lua, top))
+    {
+      result = static_cast<float>(lua_tonumber(lua, top));
+    }
+    else if (lua_isstring(lua, top))
+    {
+      result = std::string(lua_tostring(lua, top));
+    }
+    else if (lua_isboolean(lua, top))
+    {
+      result = static_cast<bool>(lua_toboolean(lua, top));
+    }
+    else
+    {
+      LOG(WARNING) << "Unsupported Lua type when returning Json";
+      result = Json::nullValue;
+    }
+  }
+
+
+  void LuaFunctionCall::ExecuteToJson(Json::Value& result)
+  {
+    ExecuteInternal(1);
+    PopJson(result, context_.lua_, lua_gettop(context_.lua_));
+  }
 }
--- a/Core/Lua/LuaFunctionCall.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Lua/LuaFunctionCall.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,18 +36,18 @@
 
 #include <json/json.h>
 
-
 namespace Orthanc
 {
   class LuaFunctionCall : public boost::noncopyable
   {
   private:
     LuaContext& context_;
-    boost::mutex::scoped_lock lock_;
     bool isExecuted_;
 
     void CheckAlreadyExecuted();
 
+    void ExecuteInternal(int numOutputs);
+
   public:
     LuaFunctionCall(LuaContext& context,
                     const char* functionName);
@@ -60,10 +60,15 @@
 
     void PushDouble(double value);
 
-    void PushJSON(const Json::Value& value);
+    void PushJson(const Json::Value& value);
 
-    void Execute(int numOutputs = 0);
+    void Execute()
+    {
+      ExecuteInternal(0);
+    }
 
     bool ExecutePredicate();
+
+    void ExecuteToJson(Json::Value& result);                    
   };
 }
--- a/Core/MultiThreading/ArrayFilledByThreads.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/ArrayFilledByThreads.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/MultiThreading/BagOfRunnablesBySteps.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/BagOfRunnablesBySteps.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/MultiThreading/BagOfRunnablesBySteps.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/BagOfRunnablesBySteps.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/MultiThreading/ILockable.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/ILockable.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/MultiThreading/IRunnableBySteps.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/IRunnableBySteps.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/MultiThreading/Locker.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/Locker.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/MultiThreading/Mutex.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/Mutex.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -37,7 +37,7 @@
 
 #if defined(_WIN32)
 #include <windows.h>
-#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__)
+#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__)
 #include <pthread.h>
 #else
 #error Support your platform here
@@ -75,7 +75,7 @@
   }
 
 
-#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__)
+#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__)
 
   struct Mutex::PImpl
   {
--- a/Core/MultiThreading/Mutex.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/Mutex.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/MultiThreading/ReaderWriterLock.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/ReaderWriterLock.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/MultiThreading/ReaderWriterLock.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/ReaderWriterLock.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/MultiThreading/Semaphore.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,67 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "Semaphore.h"
+
+#include "../OrthancException.h"
+
+
+namespace Orthanc
+{
+  Semaphore::Semaphore(unsigned int count) : count_(count)
+  {
+    if (count == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  void Semaphore::Release()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    count_++;
+    condition_.notify_one(); 
+  }
+
+  void Semaphore::Acquire()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    while (count_ == 0)
+    {
+      condition_.wait(lock);
+    }
+
+    count_++;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/MultiThreading/Semaphore.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 <boost/noncopyable.hpp>
+#include <boost/thread.hpp>
+
+namespace Orthanc
+{
+  class Semaphore : public boost::noncopyable
+  {
+  private:
+    unsigned int count_;
+    boost::mutex mutex_;
+    boost::condition_variable condition_;
+
+  public:
+    explicit Semaphore(unsigned int count);
+
+    void Release();
+
+    void Acquire();
+  };
+}
--- a/Core/MultiThreading/SharedMessageQueue.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/SharedMessageQueue.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,11 +33,40 @@
 #include "../PrecompiledHeaders.h"
 #include "SharedMessageQueue.h"
 
+
+
+/**
+ * FIFO (queue):
+ * 
+ *            back                         front
+ *            +--+--+--+--+--+--+--+--+--+--+--+
+ * Enqueue -> |  |  |  |  |  |  |  |  |  |  |  |
+ *            |  |  |  |  |  |  |  |  |  |  |  | -> Dequeue
+ *            +--+--+--+--+--+--+--+--+--+--+--+
+ *                                            ^
+ *                                            |
+ *                                      Make room here
+ *
+ *
+ * LIFO (stack):
+ * 
+ *            back                         front
+ *            +--+--+--+--+--+--+--+--+--+--+--+
+ *            |  |  |  |  |  |  |  |  |  |  |  | <- Enqueue
+ *            |  |  |  |  |  |  |  |  |  |  |  | -> Dequeue
+ *            +--+--+--+--+--+--+--+--+--+--+--+
+ *              ^
+ *              |
+ *        Make room here
+ **/
+
+
 namespace Orthanc
 {
-  SharedMessageQueue::SharedMessageQueue(unsigned int maxSize)
+  SharedMessageQueue::SharedMessageQueue(unsigned int maxSize) :
+    isFifo_(true),
+    maxSize_(maxSize)
   {
-    maxSize_ = maxSize;
   }
 
 
@@ -56,12 +85,31 @@
 
     if (maxSize_ != 0 && queue_.size() > maxSize_)
     {
-      // Too many elements in the queue: First remove the oldest
-      delete queue_.front();
-      queue_.pop_front();
+      if (isFifo_)
+      {
+        // Too many elements in the queue: Make room
+        delete queue_.front();
+        queue_.pop_front();
+      }
+      else
+      {
+        // Too many elements in the stack: Make room
+        delete queue_.back();
+        queue_.pop_back();
+      }
     }
 
-    queue_.push_back(message);
+    if (isFifo_)
+    {
+      // Queue policy (FIFO)
+      queue_.push_back(message);
+    }
+    else
+    {
+      // Stack policy (LIFO)
+      queue_.push_front(message);
+    }
+
     elementAvailable_.notify_one();
   }
 
@@ -124,4 +172,17 @@
 
     return true;
   }
+
+
+  void SharedMessageQueue::SetFifoPolicy()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    isFifo_ = true;
+  }
+
+  void SharedMessageQueue::SetLifoPolicy()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    isFifo_ = false;
+  }
 }
--- a/Core/MultiThreading/SharedMessageQueue.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/SharedMessageQueue.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -40,11 +40,12 @@
 
 namespace Orthanc
 {
-  class SharedMessageQueue
+  class SharedMessageQueue : public boost::noncopyable
   {
   private:
     typedef std::list<IDynamicObject*>  Queue;
 
+    bool isFifo_;
     unsigned int maxSize_;
     Queue queue_;
     boost::mutex mutex_;
@@ -52,8 +53,8 @@
     boost::condition_variable emptied_;
 
   public:
-    SharedMessageQueue(unsigned int maxSize = 0);
-
+    explicit SharedMessageQueue(unsigned int maxSize = 0);
+    
     ~SharedMessageQueue();
 
     // This transfers the ownership of the message
@@ -63,5 +64,19 @@
     IDynamicObject* Dequeue(int32_t millisecondsTimeout);
 
     bool WaitEmpty(int32_t millisecondsTimeout);
+
+    bool IsFifoPolicy() const
+    {
+      return isFifo_;
+    }
+
+    bool IsLifoPolicy() const
+    {
+      return !isFifo_;
+    }
+
+    void SetFifoPolicy();
+
+    void SetLifoPolicy();
   };
 }
--- a/Core/MultiThreading/ThreadedCommandProcessor.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/ThreadedCommandProcessor.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -175,7 +175,7 @@
   {
     boost::mutex::scoped_lock lock(mutex_);
 
-    while (!remainingCommands_ == 0)
+    while (remainingCommands_ != 0)
     {
       processedCommand_.wait(lock);
     }
--- a/Core/MultiThreading/ThreadedCommandProcessor.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/MultiThreading/ThreadedCommandProcessor.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/OrthancException.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/OrthancException.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -121,6 +121,18 @@
       case ErrorCode_IncompatibleImageFormat:
         return "Incompatible format of the images";
 
+      case ErrorCode_SharedLibrary:
+        return "Error while using a shared library (plugin)";
+
+      case ErrorCode_SystemCommand:
+        return "Error while calling a system command";
+
+      case ErrorCode_Plugin:
+        return "Error encountered inside a plugin";
+
+      case ErrorCode_Database:
+        return "Error with the database engine";
+
       case ErrorCode_Custom:
       default:
         return "???";
--- a/Core/OrthancException.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/OrthancException.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/PrecompiledHeaders.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/PrecompiledHeaders.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/PrecompiledHeaders.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/PrecompiledHeaders.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,12 +32,12 @@
 
 #pragma once
 
-#if ORTHANC_USE_PRECOMPILED_HEADERS == 1
-
-#ifndef NOMINMAX
+#if defined(_WIN32) && !defined(NOMINMAX)
 #define NOMINMAX
 #endif
 
+#if ORTHANC_USE_PRECOMPILED_HEADERS == 1
+
 #include <boost/date_time/posix_time/posix_time.hpp>
 #include <boost/filesystem.hpp>
 #include <boost/lexical_cast.hpp>
@@ -49,6 +49,10 @@
 #include <glog/logging.h>
 #include <json/value.h>
 
+#if ORTHANC_PUGIXML_ENABLED == 1
+#include <pugixml.hpp>
+#endif
+
 #include "Enumerations.h"
 #include "OrthancException.h"
 #include "Toolbox.h"
--- a/Core/RestApi/RestApi.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/RestApi/RestApi.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,86 +36,87 @@
 #include <stdlib.h>   // To define "_exit()" under Windows
 #include <glog/logging.h>
 
+#include <stdio.h>
+
 namespace Orthanc
 {
-  bool RestApi::Call::ParseJsonRequestInternal(Json::Value& result,
-                                               const char* request)
+  namespace
   {
-    result.clear();
-    Json::Reader reader;
-    return reader.parse(request, result);
-  }
+    // Anonymous namespace to avoid clashes between compilation modules
+    class HttpHandlerVisitor : public RestApiHierarchy::IVisitor
+    {
+    private:
+      RestApi& api_;
+      RestApiOutput& output_;
+      HttpMethod method_;
+      const HttpHandler::Arguments& headers_;
+      const HttpHandler::Arguments& getArguments_;
+      const std::string& postData_;
 
+    public:
+      HttpHandlerVisitor(RestApi& api,
+                         RestApiOutput& output,
+                         HttpMethod method,
+                         const HttpHandler::Arguments& headers,
+                         const HttpHandler::Arguments& getArguments,
+                         const std::string& postData) :
+        api_(api),
+        output_(output),
+        method_(method),
+        headers_(headers),
+        getArguments_(getArguments),
+        postData_(postData)
+      {
+      }
 
-  bool RestApi::GetCall::ParseJsonRequest(Json::Value& result) const
-  {
-    result.clear();
+      virtual bool Visit(const RestApiHierarchy::Resource& resource,
+                         const UriComponents& uri,
+                         const HttpHandler::Arguments& components,
+                         const UriComponents& trailing)
+      {
+        if (resource.HasHandler(method_))
+        {
+          switch (method_)
+          {
+            case HttpMethod_Get:
+            {
+              RestApiGetCall call(output_, api_, headers_, components, trailing, uri, getArguments_);
+              resource.Handle(call);
+              return true;
+            }
+
+            case HttpMethod_Post:
+            {
+              RestApiPostCall call(output_, api_, headers_, components, trailing, uri, postData_);
+              resource.Handle(call);
+              return true;
+            }
 
-    for (HttpHandler::Arguments::const_iterator 
-           it = getArguments_.begin(); it != getArguments_.end(); ++it)
-    {
-      result[it->first] = it->second;
-    }
+            case HttpMethod_Delete:
+            {
+              RestApiDeleteCall call(output_, api_, headers_, components, trailing, uri);
+              resource.Handle(call);
+              return true;
+            }
 
-    return true;
+            case HttpMethod_Put:
+            {
+              RestApiPutCall call(output_, api_, headers_, components, trailing, uri, postData_);
+              resource.Handle(call);
+              return true;
+            }
+
+            default:
+              return false;
+          }
+        }
+
+        return false;
+      }
+    };
   }
 
 
-  bool RestApi::IsGetAccepted(const UriComponents& uri)
-  {
-    for (GetHandlers::const_iterator it = getHandlers_.begin();
-         it != getHandlers_.end(); ++it)
-    {
-      if (it->first->Match(uri))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  bool RestApi::IsPutAccepted(const UriComponents& uri)
-  {
-    for (PutHandlers::const_iterator it = putHandlers_.begin();
-         it != putHandlers_.end(); ++it)
-    {
-      if (it->first->Match(uri))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  bool RestApi::IsPostAccepted(const UriComponents& uri)
-  {
-    for (PostHandlers::const_iterator it = postHandlers_.begin();
-         it != postHandlers_.end(); ++it)
-    {
-      if (it->first->Match(uri))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  bool RestApi::IsDeleteAccepted(const UriComponents& uri)
-  {
-    for (DeleteHandlers::const_iterator it = deleteHandlers_.begin();
-         it != deleteHandlers_.end(); ++it)
-    {
-      if (it->first->Match(uri))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
 
   static void AddMethod(std::string& target,
                         const std::string& method)
@@ -126,158 +127,128 @@
       target = method;
   }
 
-  std::string  RestApi::GetAcceptedMethods(const UriComponents& uri)
+  static std::string  MethodsToString(const std::set<HttpMethod>& methods)
   {
     std::string s;
 
-    if (IsGetAccepted(uri))
+    if (methods.find(HttpMethod_Get) != methods.end())
+    {
       AddMethod(s, "GET");
+    }
 
-    if (IsPutAccepted(uri))
-      AddMethod(s, "PUT");
+    if (methods.find(HttpMethod_Post) != methods.end())
+    {
+      AddMethod(s, "POST");
+    }
 
-    if (IsPostAccepted(uri))
-      AddMethod(s, "POST");
+    if (methods.find(HttpMethod_Put) != methods.end())
+    {
+      AddMethod(s, "PUT");
+    }
 
-    if (IsDeleteAccepted(uri))
+    if (methods.find(HttpMethod_Delete) != methods.end())
+    {
       AddMethod(s, "DELETE");
+    }
 
     return s;
   }
 
-  RestApi::~RestApi()
-  {
-    for (GetHandlers::iterator it = getHandlers_.begin(); 
-         it != getHandlers_.end(); ++it)
-    {
-      delete it->first;
-    } 
 
-    for (PutHandlers::iterator it = putHandlers_.begin(); 
-         it != putHandlers_.end(); ++it)
-    {
-      delete it->first;
-    } 
 
-    for (PostHandlers::iterator it = postHandlers_.begin(); 
-         it != postHandlers_.end(); ++it)
-    {
-      delete it->first;
-    } 
-
-    for (DeleteHandlers::iterator it = deleteHandlers_.begin(); 
-         it != deleteHandlers_.end(); ++it)
-    {
-      delete it->first;
-    } 
-  }
-
-  bool RestApi::IsServedUri(const UriComponents& uri)
-  {
-    return (IsGetAccepted(uri) ||
-            IsPutAccepted(uri) ||
-            IsPostAccepted(uri) ||
-            IsDeleteAccepted(uri));
-  }
-
-  void RestApi::Handle(HttpOutput& output,
+  bool RestApi::Handle(HttpOutput& output,
                        HttpMethod method,
                        const UriComponents& uri,
                        const Arguments& headers,
-                       const Arguments& getArguments,
+                       const GetArguments& getArguments,
                        const std::string& postData)
   {
-    bool ok = false;
-    RestApiOutput restOutput(output);
-    RestApiPath::Components components;
-    UriComponents trailing;
+    RestApiOutput wrappedOutput(output);
 
-    if (method == HttpMethod_Get)
+#if ORTHANC_PUGIXML_ENABLED == 1
+    // Look if the user wishes XML answers instead of JSON
+    // http://www.w3.org/Protocols/HTTP/HTRQ_Headers.html#z3
+    Arguments::const_iterator it = headers.find("accept");
+    if (it != headers.end())
     {
-      for (GetHandlers::const_iterator it = getHandlers_.begin();
-           it != getHandlers_.end(); ++it)
+      std::vector<std::string> accepted;
+      Toolbox::TokenizeString(accepted, it->second, ';');
+      for (size_t i = 0; i < accepted.size(); i++)
       {
-        if (it->first->Match(components, trailing, uri))
+        if (accepted[i] == "application/xml")
         {
-          //LOG(INFO) << "REST GET call on: " << Toolbox::FlattenUri(uri);
-          ok = true;
-          GetCall call(restOutput, *this, headers, components, trailing, uri, getArguments);
-          it->second(call);
+          wrappedOutput.SetConvertJsonToXml(true);
+        }
+
+        if (accepted[i] == "application/json")
+        {
+          wrappedOutput.SetConvertJsonToXml(false);
         }
       }
     }
-    else if (method == HttpMethod_Put)
-    {
-      for (PutHandlers::const_iterator it = putHandlers_.begin();
-           it != putHandlers_.end(); ++it)
-      {
-        if (it->first->Match(components, trailing, uri))
-        {
-          //LOG(INFO) << "REST PUT call on: " << Toolbox::FlattenUri(uri);
-          ok = true;
-          PutCall call(restOutput, *this, headers, components, trailing, uri, postData);
-          it->second(call);
-        }
-      }
-    }
-    else if (method == HttpMethod_Post)
+#endif
+
+    Arguments compiled;
+    HttpHandler::CompileGetArguments(compiled, getArguments);
+
+    HttpHandlerVisitor visitor(*this, wrappedOutput, method, headers, compiled, postData);
+
+    if (root_.LookupResource(uri, visitor))
     {
-      for (PostHandlers::const_iterator it = postHandlers_.begin();
-           it != postHandlers_.end(); ++it)
-      {
-        if (it->first->Match(components, trailing, uri))
-        {
-          //LOG(INFO) << "REST POST call on: " << Toolbox::FlattenUri(uri);
-          ok = true;
-          PostCall call(restOutput, *this, headers, components, trailing, uri, postData);
-          it->second(call);
-        }
-      }
-    }
-    else if (method == HttpMethod_Delete)
-    {
-      for (DeleteHandlers::const_iterator it = deleteHandlers_.begin();
-           it != deleteHandlers_.end(); ++it)
-      {
-        if (it->first->Match(components, trailing, uri))
-        {
-          //LOG(INFO) << "REST DELETE call on: " << Toolbox::FlattenUri(uri);
-          ok = true;
-          DeleteCall call(restOutput, *this, headers, components, trailing, uri);
-          it->second(call);
-        }
-      }
+      wrappedOutput.Finalize();
+      return true;
     }
 
-    if (!ok)
+    std::set<HttpMethod> methods;
+    root_.GetAcceptedMethods(methods, uri);
+
+    if (methods.empty())
+    {
+      return false;  // This URI is not served by this REST API
+    }
+    else
     {
       LOG(INFO) << "REST method " << EnumerationToString(method) 
                 << " not allowed on: " << Toolbox::FlattenUri(uri);
-      output.SendMethodNotAllowedError(GetAcceptedMethods(uri));
+
+      output.SendMethodNotAllowed(MethodsToString(methods));
+
+      return true;
     }
   }
 
   void RestApi::Register(const std::string& path,
-                         GetHandler handler)
+                         RestApiGetCall::Handler handler)
   {
-    getHandlers_.push_back(std::make_pair(new RestApiPath(path), handler));
+    root_.Register(path, handler);
   }
 
   void RestApi::Register(const std::string& path,
-                         PutHandler handler)
+                         RestApiPutCall::Handler handler)
   {
-    putHandlers_.push_back(std::make_pair(new RestApiPath(path), handler));
+    root_.Register(path, handler);
   }
 
   void RestApi::Register(const std::string& path,
-                         PostHandler handler)
+                         RestApiPostCall::Handler handler)
   {
-    postHandlers_.push_back(std::make_pair(new RestApiPath(path), handler));
+    root_.Register(path, handler);
   }
 
   void RestApi::Register(const std::string& path,
-                         DeleteHandler handler)
+                         RestApiDeleteCall::Handler handler)
+  {
+    root_.Register(path, handler);
+  }
+  
+  void RestApi::AutoListChildren(RestApiGetCall& call)
   {
-    deleteHandlers_.push_back(std::make_pair(new RestApiPath(path), handler));
+    RestApi& context = call.GetContext();
+
+    Json::Value directory;
+    if (context.root_.GetDirectory(directory, call.GetFullUri()))
+    {
+      call.GetOutput().AnswerJson(directory);
+    }    
   }
 }
--- a/Core/RestApi/RestApi.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/RestApi/RestApi.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,9 +32,7 @@
 
 #pragma once
 
-#include "../HttpServer/HttpHandler.h"
-#include "RestApiPath.h"
-#include "RestApiOutput.h"
+#include "RestApiHierarchy.h"
 
 #include <list>
 
@@ -42,254 +40,29 @@
 {
   class RestApi : public HttpHandler
   {
-  public:
-    class Call
-    {
-      friend class RestApi;
-
-    private:
-      RestApiOutput& output_;
-      RestApi& context_;
-      const HttpHandler::Arguments& httpHeaders_;
-      const RestApiPath::Components& uriComponents_;
-      const UriComponents& trailing_;
-      const UriComponents& fullUri_;
-
-      Call(RestApiOutput& output,
-           RestApi& context,
-           const HttpHandler::Arguments& httpHeaders,
-           const RestApiPath::Components& uriComponents,
-           const UriComponents& trailing,
-           const UriComponents& fullUri) :
-        output_(output),
-        context_(context),
-        httpHeaders_(httpHeaders),
-        uriComponents_(uriComponents),
-        trailing_(trailing),
-        fullUri_(fullUri)
-      {
-      }
-
-    protected:
-      static bool ParseJsonRequestInternal(Json::Value& result,
-                                           const char* request);
-
-    public:
-      RestApiOutput& GetOutput()
-      {
-        return output_;
-      }
-
-      RestApi& GetContext()
-      {
-        return context_;
-      }
-    
-      const UriComponents& GetFullUri() const
-      {
-        return fullUri_;
-      }
-    
-      const UriComponents& GetTrailingUri() const
-      {
-        return trailing_;
-      }
-
-      std::string GetUriComponent(const std::string& name,
-                                  const std::string& defaultValue) const
-      {
-        return HttpHandler::GetArgument(uriComponents_, name, defaultValue);
-      }
-
-      std::string GetHttpHeader(const std::string& name,
-                                const std::string& defaultValue) const
-      {
-        return HttpHandler::GetArgument(httpHeaders_, name, defaultValue);
-      }
-
-      const HttpHandler::Arguments& GetHttpHeaders() const
-      {
-        return httpHeaders_;
-      }
-
-      void ParseCookies(HttpHandler::Arguments& result) const
-      {
-        HttpHandler::ParseCookies(result, httpHeaders_);
-      }
-
-      virtual bool ParseJsonRequest(Json::Value& result) const = 0;
-    };
-
- 
-    class GetCall : public Call
-    {
-      friend class RestApi;
-
-    private:
-      const HttpHandler::Arguments& getArguments_;
-
-    public:
-      GetCall(RestApiOutput& output,
-              RestApi& context,
-              const HttpHandler::Arguments& httpHeaders,
-              const RestApiPath::Components& uriComponents,
-              const UriComponents& trailing,
-              const UriComponents& fullUri,
-              const HttpHandler::Arguments& getArguments) :
-        Call(output, context, httpHeaders, uriComponents, trailing, fullUri),
-        getArguments_(getArguments)
-      {
-      }
-
-      std::string GetArgument(const std::string& name,
-                              const std::string& defaultValue) const
-      {
-        return HttpHandler::GetArgument(getArguments_, name, defaultValue);
-      }
-
-      bool HasArgument(const std::string& name) const
-      {
-        return getArguments_.find(name) != getArguments_.end();
-      }
-
-      virtual bool ParseJsonRequest(Json::Value& result) const;
-    };
-
-
-    class PutCall : public Call
-    {
-      friend class RestApi;
-
-    private:
-      const std::string& data_;
-
-    public:
-      PutCall(RestApiOutput& output,
-              RestApi& context,
-              const HttpHandler::Arguments& httpHeaders,
-              const RestApiPath::Components& uriComponents,
-              const UriComponents& trailing,
-              const UriComponents& fullUri,
-              const std::string& data) :
-        Call(output, context, httpHeaders, uriComponents, trailing, fullUri),
-        data_(data)
-      {
-      }
-
-      const std::string& GetPutBody() const
-      {
-        return data_;
-      }
-
-      virtual bool ParseJsonRequest(Json::Value& result) const
-      {
-        return ParseJsonRequestInternal(result, GetPutBody().c_str());
-      }      
-    };
-
-    class PostCall : public Call
-    {
-      friend class RestApi;
-
-    private:
-      const std::string& data_;
-
-    public:
-      PostCall(RestApiOutput& output,
-               RestApi& context,
-               const HttpHandler::Arguments& httpHeaders,
-               const RestApiPath::Components& uriComponents,
-               const UriComponents& trailing,
-               const UriComponents& fullUri,
-               const std::string& data) :
-        Call(output, context, httpHeaders, uriComponents, trailing, fullUri),
-        data_(data)
-      {
-      }
-
-      const std::string& GetPostBody() const
-      {
-        return data_;
-      }
-
-      virtual bool ParseJsonRequest(Json::Value& result) const
-      {
-        return ParseJsonRequestInternal(result, GetPostBody().c_str());
-      }      
-    };
-
-    class DeleteCall : public Call
-    {
-    public:
-      DeleteCall(RestApiOutput& output,
-                 RestApi& context,
-                 const HttpHandler::Arguments& httpHeaders,
-                 const RestApiPath::Components& uriComponents,
-                 const UriComponents& trailing,
-                 const UriComponents& fullUri) :
-        Call(output, context, httpHeaders, uriComponents, trailing, fullUri)
-      {
-      }
-
-      virtual bool ParseJsonRequest(Json::Value& result) const
-      {
-        result.clear();
-        return true;
-      }
-    };
-
-    typedef void (*GetHandler) (GetCall& call);
-    
-    typedef void (*DeleteHandler) (DeleteCall& call);
-    
-    typedef void (*PutHandler) (PutCall& call);
-    
-    typedef void (*PostHandler) (PostCall& call);
-    
   private:
-    typedef std::list< std::pair<RestApiPath*, GetHandler> > GetHandlers;
-    typedef std::list< std::pair<RestApiPath*, PutHandler> > PutHandlers;
-    typedef std::list< std::pair<RestApiPath*, PostHandler> > PostHandlers;
-    typedef std::list< std::pair<RestApiPath*, DeleteHandler> > DeleteHandlers;
-
-    GetHandlers  getHandlers_;
-    PutHandlers  putHandlers_;
-    PostHandlers  postHandlers_;
-    DeleteHandlers  deleteHandlers_;
-
-    bool IsGetAccepted(const UriComponents& uri);
-    bool IsPutAccepted(const UriComponents& uri);
-    bool IsPostAccepted(const UriComponents& uri);
-    bool IsDeleteAccepted(const UriComponents& uri);
-
-    std::string  GetAcceptedMethods(const UriComponents& uri);
+    RestApiHierarchy root_;
 
   public:
-    RestApi()
-    {
-    }
+    static void AutoListChildren(RestApiGetCall& call);
 
-    ~RestApi();
-
-    virtual bool IsServedUri(const UriComponents& uri);
-
-    virtual void Handle(HttpOutput& output,
+    virtual bool Handle(HttpOutput& output,
                         HttpMethod method,
                         const UriComponents& uri,
                         const Arguments& headers,
-                        const Arguments& getArguments,
+                        const GetArguments& getArguments,
                         const std::string& postData);
 
     void Register(const std::string& path,
-                  GetHandler handler);
+                  RestApiGetCall::Handler handler);
 
     void Register(const std::string& path,
-                  PutHandler handler);
+                  RestApiPutCall::Handler handler);
 
     void Register(const std::string& path,
-                  PostHandler handler);
+                  RestApiPostCall::Handler handler);
 
     void Register(const std::string& path,
-                  DeleteHandler handler);
+                  RestApiDeleteCall::Handler handler);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiCall.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,44 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "RestApiCall.h"
+
+namespace Orthanc
+{
+  bool RestApiCall::ParseJsonRequestInternal(Json::Value& result,
+                                             const char* request)
+  {
+    result.clear();
+    Json::Reader reader;
+    return reader.parse(request, result);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiCall.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,119 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "../HttpServer/HttpHandler.h"
+#include "RestApiPath.h"
+#include "RestApiOutput.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class RestApi;
+
+  class RestApiCall : public boost::noncopyable
+  {
+  private:
+    RestApiOutput& output_;
+    RestApi& context_;
+    const HttpHandler::Arguments& httpHeaders_;
+    const HttpHandler::Arguments& uriComponents_;
+    const UriComponents& trailing_;
+    const UriComponents& fullUri_;
+
+  protected:
+    static bool ParseJsonRequestInternal(Json::Value& result,
+                                         const char* request);
+
+  public:
+    RestApiCall(RestApiOutput& output,
+                RestApi& context,
+                const HttpHandler::Arguments& httpHeaders,
+                const HttpHandler::Arguments& uriComponents,
+                const UriComponents& trailing,
+                const UriComponents& fullUri) :
+      output_(output),
+      context_(context),
+      httpHeaders_(httpHeaders),
+      uriComponents_(uriComponents),
+      trailing_(trailing),
+      fullUri_(fullUri)
+    {
+    }
+
+    RestApiOutput& GetOutput()
+    {
+      return output_;
+    }
+
+    RestApi& GetContext()
+    {
+      return context_;
+    }
+    
+    const UriComponents& GetFullUri() const
+    {
+      return fullUri_;
+    }
+    
+    const UriComponents& GetTrailingUri() const
+    {
+      return trailing_;
+    }
+
+    std::string GetUriComponent(const std::string& name,
+                                const std::string& defaultValue) const
+    {
+      return HttpHandler::GetArgument(uriComponents_, name, defaultValue);
+    }
+
+    std::string GetHttpHeader(const std::string& name,
+                              const std::string& defaultValue) const
+    {
+      return HttpHandler::GetArgument(httpHeaders_, name, defaultValue);
+    }
+
+    const HttpHandler::Arguments& GetHttpHeaders() const
+    {
+      return httpHeaders_;
+    }
+
+    void ParseCookies(HttpHandler::Arguments& result) const
+    {
+      HttpHandler::ParseCookies(result, httpHeaders_);
+    }
+
+    virtual bool ParseJsonRequest(Json::Value& result) const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiDeleteCall.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,60 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "RestApiCall.h"
+
+namespace Orthanc
+{
+  class RestApiDeleteCall : public RestApiCall
+  {
+  public:
+    typedef void (*Handler) (RestApiDeleteCall& call);
+    
+    RestApiDeleteCall(RestApiOutput& output,
+                      RestApi& context,
+                      const HttpHandler::Arguments& httpHeaders,
+                      const HttpHandler::Arguments& uriComponents,
+                      const UriComponents& trailing,
+                      const UriComponents& fullUri) :
+      RestApiCall(output, context, httpHeaders, uriComponents, trailing, fullUri)
+    {
+    }
+
+    virtual bool ParseJsonRequest(Json::Value& result) const
+    {
+      result.clear();
+      return true;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiGetCall.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,49 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "RestApiGetCall.h"
+
+namespace Orthanc
+{
+  bool RestApiGetCall::ParseJsonRequest(Json::Value& result) const
+  {
+    result.clear();
+
+    for (HttpHandler::Arguments::const_iterator 
+           it = getArguments_.begin(); it != getArguments_.end(); ++it)
+    {
+      result[it->first] = it->second;
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiGetCall.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,72 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "RestApiCall.h"
+
+namespace Orthanc
+{
+  class RestApiGetCall : public RestApiCall
+  {
+  private:
+    const HttpHandler::Arguments& getArguments_;
+
+  public:
+    typedef void (*Handler) (RestApiGetCall& call);   
+
+    RestApiGetCall(RestApiOutput& output,
+                   RestApi& context,
+                   const HttpHandler::Arguments& httpHeaders,
+                   const HttpHandler::Arguments& uriComponents,
+                   const UriComponents& trailing,
+                   const UriComponents& fullUri,
+                   const HttpHandler::Arguments& getArguments) :
+      RestApiCall(output, context, httpHeaders, uriComponents, trailing, fullUri),
+      getArguments_(getArguments)
+    {
+    }
+
+    std::string GetArgument(const std::string& name,
+                            const std::string& defaultValue) const
+    {
+      return HttpHandler::GetArgument(getArguments_, name, defaultValue);
+    }
+
+    bool HasArgument(const std::string& name) const
+    {
+      return getArguments_.find(name) != getArguments_.end();
+    }
+
+    virtual bool ParseJsonRequest(Json::Value& result) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiHierarchy.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,473 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "RestApiHierarchy.h"
+
+#include "../OrthancException.h"
+
+#include <cassert>
+#include <stdio.h>
+
+namespace Orthanc
+{
+  RestApiHierarchy::Resource::Resource() : 
+    getHandler_(NULL), 
+    postHandler_(NULL),
+    putHandler_(NULL), 
+    deleteHandler_(NULL)
+  {
+  }
+
+
+  bool RestApiHierarchy::Resource::HasHandler(HttpMethod method) const
+  {
+    switch (method)
+    {
+      case HttpMethod_Get:
+        return getHandler_ != NULL;
+
+      case HttpMethod_Post:
+        return postHandler_ != NULL;
+
+      case HttpMethod_Put:
+        return putHandler_ != NULL;
+
+      case HttpMethod_Delete:
+        return deleteHandler_ != NULL;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool RestApiHierarchy::Resource::IsEmpty() const
+  {
+    return (getHandler_ == NULL &&
+            postHandler_ == NULL &&
+            putHandler_ == NULL &&
+            deleteHandler_ == NULL);
+  }
+
+
+  RestApiHierarchy& RestApiHierarchy::AddChild(Children& children,
+                                               const std::string& name)
+  {
+    Children::iterator it = children.find(name);
+
+    if (it == children.end())
+    {
+      // Create new child
+      RestApiHierarchy *child = new RestApiHierarchy;
+      children[name] = child;
+      return *child;
+    }
+    else
+    {
+      return *it->second;
+    }
+  }
+
+
+
+  bool RestApiHierarchy::Resource::Handle(RestApiGetCall& call) const
+  {
+    if (getHandler_ != NULL)
+    {
+      getHandler_(call);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool RestApiHierarchy::Resource::Handle(RestApiPutCall& call) const
+  {
+    if (putHandler_ != NULL)
+    {
+      putHandler_(call);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool RestApiHierarchy::Resource::Handle(RestApiPostCall& call) const
+  {
+    if (postHandler_ != NULL)
+    {
+      postHandler_(call);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool RestApiHierarchy::Resource::Handle(RestApiDeleteCall& call) const
+  {
+    if (deleteHandler_ != NULL)
+    {
+      deleteHandler_(call);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+
+  void RestApiHierarchy::DeleteChildren(Children& children)
+  {
+    for (Children::iterator it = children.begin();
+         it != children.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+
+  template <typename Handler>
+  void RestApiHierarchy::RegisterInternal(const RestApiPath& path,
+                                          Handler handler,
+                                          size_t level)
+  {
+    if (path.GetLevelCount() == level)
+    {
+      if (path.IsUniversalTrailing())
+      {
+        universalHandlers_.Register(handler);
+      }
+      else
+      {
+        handlers_.Register(handler);
+      }
+    }
+    else
+    {
+      RestApiHierarchy* child;
+      if (path.IsWildcardLevel(level))
+      {
+        child = &AddChild(wildcardChildren_, path.GetWildcardName(level));
+      }
+      else
+      {
+        child = &AddChild(children_, path.GetLevelName(level));
+      }
+
+      child->RegisterInternal(path, handler, level + 1);
+    }
+  }
+
+
+  bool RestApiHierarchy::LookupResource(HttpHandler::Arguments& components,
+                                       const UriComponents& uri,
+                                       IVisitor& visitor,
+                                       size_t level)
+  {
+    if (uri.size() != 0 &&
+        level > uri.size())
+    {
+      return false;
+    }
+
+    UriComponents trailing;
+
+    // Look for an exact match on the resource of interest
+      if (uri.size() == 0 ||
+          level == uri.size())
+    {
+      if (!handlers_.IsEmpty() &&
+          visitor.Visit(handlers_, uri, components, trailing))
+      {
+        return true;
+      }
+    }
+
+
+    if (level < uri.size())  // A recursive call is possible
+    {
+      // Try and go down in the hierarchy, using an exact match for the child
+      Children::const_iterator child = children_.find(uri[level]);
+      if (child != children_.end())
+      {
+        if (child->second->LookupResource(components, uri, visitor, level + 1))
+        {
+          return true;
+        }
+      }
+
+      // Try and go down in the hierarchy, using wildcard rules for children
+      for (child = wildcardChildren_.begin();
+           child != wildcardChildren_.end(); ++child)
+      {
+        HttpHandler::Arguments subComponents = components;
+        subComponents[child->first] = uri[level];
+
+        if (child->second->LookupResource(subComponents, uri, visitor, level + 1))
+        {
+          return true;
+        }        
+      }
+    }
+
+
+    // As a last resort, call the universal handlers, if any
+    if (!universalHandlers_.IsEmpty())
+    {
+      trailing.resize(uri.size() - level);
+      size_t pos = 0;
+      for (size_t i = level; i < uri.size(); i++, pos++)
+      {
+        trailing[pos] = uri[i];
+      }
+
+      assert(pos == trailing.size());
+
+      if (visitor.Visit(universalHandlers_, uri, components, trailing))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  bool RestApiHierarchy::CanGenerateDirectory() const
+  {
+    return (universalHandlers_.IsEmpty() &&
+            wildcardChildren_.empty());
+  }
+
+
+  bool RestApiHierarchy::GetDirectory(Json::Value& result,
+                                      const UriComponents& uri,
+                                      size_t level)
+  {
+    if (uri.size() == level)
+    {
+      if (CanGenerateDirectory())
+      {
+        result = Json::arrayValue;
+
+        for (Children::const_iterator it = children_.begin();
+             it != children_.end(); ++it)
+        {
+          result.append(it->first);
+        }
+
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    Children::const_iterator child = children_.find(uri[level]);
+    if (child != children_.end())
+    {
+      if (child->second->GetDirectory(result, uri, level + 1))
+      {
+        return true;
+      }
+    }
+
+    for (child = wildcardChildren_.begin(); 
+         child != wildcardChildren_.end(); ++child)
+    {
+      if (child->second->GetDirectory(result, uri, level + 1))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+                       
+
+  RestApiHierarchy::~RestApiHierarchy()
+  {
+    DeleteChildren(children_);
+    DeleteChildren(wildcardChildren_);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApiGetCall::Handler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApiPutCall::Handler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApiPostCall::Handler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApiDeleteCall::Handler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::CreateSiteMap(Json::Value& target) const
+  {
+    target = Json::objectValue;
+
+    /*std::string s = " ";
+      if (handlers_.HasHandler(HttpMethod_Get))
+      {
+      s += "GET ";
+      }
+
+      if (handlers_.HasHandler(HttpMethod_Post))
+      {
+      s += "POST ";
+      }
+
+      if (handlers_.HasHandler(HttpMethod_Put))
+      {
+      s += "PUT ";
+      }
+
+      if (handlers_.HasHandler(HttpMethod_Delete))
+      {
+      s += "DELETE ";
+      }
+
+      target = s;*/
+      
+    for (Children::const_iterator it = children_.begin();
+         it != children_.end(); ++it)
+    {
+      it->second->CreateSiteMap(target[it->first]);
+    }
+      
+    for (Children::const_iterator it = wildcardChildren_.begin();
+         it != wildcardChildren_.end(); ++it)
+    {
+      it->second->CreateSiteMap(target["<" + it->first + ">"]);
+    }
+  }
+
+
+  bool RestApiHierarchy::LookupResource(const UriComponents& uri,
+                                        IVisitor& visitor)
+  {
+    HttpHandler::Arguments components;
+    return LookupResource(components, uri, visitor, 0);
+  }    
+
+
+
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+
+    class AcceptedMethodsVisitor : public RestApiHierarchy::IVisitor
+    {
+    private:
+      std::set<HttpMethod>& methods_;
+
+    public:
+      AcceptedMethodsVisitor(std::set<HttpMethod>& methods) : methods_(methods)
+      {
+      }
+
+      virtual bool Visit(const RestApiHierarchy::Resource& resource,
+                         const UriComponents& uri,
+                         const HttpHandler::Arguments& components,
+                         const UriComponents& trailing)
+      {
+        if (trailing.size() == 0)  // Ignore universal handlers
+        {
+          if (resource.HasHandler(HttpMethod_Get))
+          {
+            methods_.insert(HttpMethod_Get);
+          }
+
+          if (resource.HasHandler(HttpMethod_Post))
+          {
+            methods_.insert(HttpMethod_Post);
+          }
+
+          if (resource.HasHandler(HttpMethod_Put))
+          {
+            methods_.insert(HttpMethod_Put);
+          }
+
+          if (resource.HasHandler(HttpMethod_Delete))
+          {
+            methods_.insert(HttpMethod_Delete);
+          }
+        }
+
+        return false;  // Continue to check all the possible ways to access this URI
+      }
+    };
+  }
+
+  void RestApiHierarchy::GetAcceptedMethods(std::set<HttpMethod>& methods,
+                                            const UriComponents& uri)
+  {
+    HttpHandler::Arguments components;
+    AcceptedMethodsVisitor visitor(methods);
+    LookupResource(components, uri, visitor, 0);
+
+    Json::Value d;
+    if (GetDirectory(d, uri))
+    {
+      methods.insert(HttpMethod_Get);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiHierarchy.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,164 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "RestApiGetCall.h"
+#include "RestApiPostCall.h"
+#include "RestApiPutCall.h"
+#include "RestApiDeleteCall.h"
+
+#include <set>
+
+namespace Orthanc
+{
+  class RestApiHierarchy : public boost::noncopyable
+  {
+  public:
+    class Resource : public boost::noncopyable
+    {
+    private:
+      RestApiGetCall::Handler     getHandler_;
+      RestApiPostCall::Handler    postHandler_;
+      RestApiPutCall::Handler     putHandler_;
+      RestApiDeleteCall::Handler  deleteHandler_;
+
+    public:
+      Resource();
+
+      bool HasHandler(HttpMethod method) const;
+
+      void Register(RestApiGetCall::Handler handler)
+      {
+        getHandler_ = handler;
+      }
+
+      void Register(RestApiPutCall::Handler handler)
+      {
+        putHandler_ = handler;
+      }
+
+      void Register(RestApiPostCall::Handler handler)
+      {
+        postHandler_ = handler;
+      }
+
+      void Register(RestApiDeleteCall::Handler handler)
+      {
+        deleteHandler_ = handler;
+      }
+
+      bool IsEmpty() const;
+
+      bool Handle(RestApiGetCall& call) const;
+
+      bool Handle(RestApiPutCall& call) const;
+
+      bool Handle(RestApiPostCall& call) const;
+
+      bool Handle(RestApiDeleteCall& call) const;
+    };
+
+
+    class IVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~IVisitor()
+      {
+      }
+
+      virtual bool Visit(const Resource& resource,
+                         const UriComponents& uri,
+                         const HttpHandler::Arguments& components,
+                         const UriComponents& trailing) = 0;
+    };
+
+
+  private:
+    typedef std::map<std::string, RestApiHierarchy*>  Children;
+
+    Resource  handlers_;
+    Children  children_;
+    Children  wildcardChildren_;
+    Resource  universalHandlers_;
+
+    static RestApiHierarchy& AddChild(Children& children,
+                                      const std::string& name);
+
+    static void DeleteChildren(Children& children);
+
+    template <typename Handler>
+    void RegisterInternal(const RestApiPath& path,
+                          Handler handler,
+                          size_t level);
+
+    bool CanGenerateDirectory() const;
+
+    bool LookupResource(HttpHandler::Arguments& components,
+                        const UriComponents& uri,
+                        IVisitor& visitor,
+                        size_t level);
+
+    bool GetDirectory(Json::Value& result,
+                      const UriComponents& uri,
+                      size_t level);
+
+  public:
+    ~RestApiHierarchy();
+
+    void Register(const std::string& uri,
+                  RestApiGetCall::Handler handler);
+
+    void Register(const std::string& uri,
+                  RestApiPutCall::Handler handler);
+
+    void Register(const std::string& uri,
+                  RestApiPostCall::Handler handler);
+
+    void Register(const std::string& uri,
+                  RestApiDeleteCall::Handler handler);
+
+    void CreateSiteMap(Json::Value& target) const;
+
+    bool GetDirectory(Json::Value& result,
+                      const UriComponents& uri)
+    {
+      return GetDirectory(result, uri, 0);
+    }
+
+    bool LookupResource(const UriComponents& uri,
+                        IVisitor& visitor);
+
+    void GetAcceptedMethods(std::set<HttpMethod>& methods,
+                            const UriComponents& uri);
+  };
+}
--- a/Core/RestApi/RestApiOutput.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/RestApi/RestApiOutput.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,22 +34,28 @@
 #include "RestApiOutput.h"
 
 #include <boost/lexical_cast.hpp>
+#include <glog/logging.h>
 
 #include "../OrthancException.h"
 
 namespace Orthanc
 {
   RestApiOutput::RestApiOutput(HttpOutput& output) : 
-    output_(output)
+    output_(output),
+    convertJsonToXml_(false)
   {
     alreadySent_ = false;
   }
 
   RestApiOutput::~RestApiOutput()
   {
+  }
+
+  void RestApiOutput::Finalize()
+  {
     if (!alreadySent_)
     {
-      output_.SendHeader(HttpStatus_400_BadRequest);
+      output_.SendStatus(HttpStatus_404_NotFound);
     }
   }
   
@@ -71,9 +77,26 @@
   void RestApiOutput::AnswerJson(const Json::Value& value)
   {
     CheckStatus();
-    Json::StyledWriter writer;
-    std::string s = writer.write(value);
-    output_.AnswerBufferWithContentType(s, "application/json", cookies_);
+
+    if (convertJsonToXml_)
+    {
+#if ORTHANC_PUGIXML_ENABLED == 1
+      std::string s;
+      Toolbox::JsonToXml(s, value);
+      output_.SetContentType("application/xml");
+      output_.SendBody(s);
+#else
+      LOG(ERROR) << "Orthanc was compiled without XML support";
+      throw OrthancException(ErrorCode_InternalError);
+#endif
+    }
+    else
+    {
+      Json::StyledWriter writer;
+      output_.SetContentType("application/json");
+      output_.SendBody(writer.write(value));
+    }
+
     alreadySent_ = true;
   }
 
@@ -81,7 +104,8 @@
                                    const std::string& contentType)
   {
     CheckStatus();
-    output_.AnswerBufferWithContentType(buffer, contentType, cookies_);
+    output_.SetContentType(contentType.c_str());
+    output_.SendBody(buffer);
     alreadySent_ = true;
   }
 
@@ -90,7 +114,8 @@
                                    const std::string& contentType)
   {
     CheckStatus();
-    output_.AnswerBufferWithContentType(buffer, length, contentType, cookies_);
+    output_.SetContentType(contentType.c_str());
+    output_.SendBody(buffer, length);
     alreadySent_ = true;
   }
 
@@ -103,14 +128,16 @@
 
   void RestApiOutput::SignalError(HttpStatus status)
   {
-    if (status != HttpStatus_403_Forbidden &&
+    if (status != HttpStatus_400_BadRequest &&
+        status != HttpStatus_403_Forbidden &&
+        status != HttpStatus_500_InternalServerError &&
         status != HttpStatus_415_UnsupportedMediaType)
     {
       throw OrthancException("This HTTP status is not allowed in a REST API");
     }
 
     CheckStatus();
-    output_.SendHeader(status);
+    output_.SendStatus(status);
     alreadySent_ = true;    
   }
 
@@ -135,7 +162,7 @@
       v += ";max-age=" + boost::lexical_cast<std::string>(maxAge);
     }
 
-    cookies_[name] = v;
+    output_.SetCookie(name, v);
   }
 
   void RestApiOutput::ResetCookie(const std::string& name)
--- a/Core/RestApi/RestApiOutput.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/RestApi/RestApiOutput.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -44,7 +44,7 @@
   private:
     HttpOutput& output_;
     bool alreadySent_;
-    HttpHandler::Arguments cookies_;
+    bool convertJsonToXml_;
 
     void CheckStatus();
 
@@ -63,6 +63,16 @@
       alreadySent_ = true;
     }
 
+    void SetConvertJsonToXml(bool convert)
+    {
+      convertJsonToXml_ = convert;
+    }
+
+    bool IsConvertJsonToXml() const
+    {
+      return convertJsonToXml_;
+    }
+
     void AnswerFile(HttpFileSender& sender);
 
     void AnswerJson(const Json::Value& value);
@@ -83,5 +93,7 @@
                    unsigned int maxAge = 0);
 
     void ResetCookie(const std::string& name);
+
+    void Finalize();
   };
 }
--- a/Core/RestApi/RestApiPath.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/RestApi/RestApiPath.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,6 +33,8 @@
 #include "../PrecompiledHeaders.h"
 #include "RestApiPath.h"
 
+#include "../OrthancException.h"
+
 #include <cassert>
 
 namespace Orthanc
@@ -76,7 +78,7 @@
     }
   }
 
-  bool RestApiPath::Match(Components& components,
+  bool RestApiPath::Match(HttpHandler::Arguments& components,
                           UriComponents& trailing,
                           const std::string& uriRaw) const
   {
@@ -85,10 +87,12 @@
     return Match(components, trailing, uri);
   }
 
-  bool RestApiPath::Match(Components& components,
+  bool RestApiPath::Match(HttpHandler::Arguments& components,
                           UriComponents& trailing,
                           const UriComponents& uri) const
   {
+    assert(uri_.size() == components_.size());
+
     if (uri.size() < uri_.size())
     {
       return false;
@@ -131,8 +135,46 @@
 
   bool RestApiPath::Match(const UriComponents& uri) const
   {
-    Components components;
+    HttpHandler::Arguments components;
     UriComponents trailing;
     return Match(components, trailing, uri);
   }
+
+
+  bool RestApiPath::IsWildcardLevel(size_t level) const
+  {
+    assert(uri_.size() == components_.size());
+
+    if (level >= uri_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    return uri_[level].length() == 0;
+  }
+
+  const std::string& RestApiPath::GetWildcardName(size_t level) const
+  {
+    assert(uri_.size() == components_.size());
+
+    if (!IsWildcardLevel(level))
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    return components_[level];
+  }
+
+  const std::string& RestApiPath::GetLevelName(size_t level) const
+  {
+    assert(uri_.size() == components_.size());
+
+    if (IsWildcardLevel(level))
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    return uri_[level];
+  }
 }
+
--- a/Core/RestApi/RestApiPath.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/RestApi/RestApiPath.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,6 +33,8 @@
 #pragma once
 
 #include "../Toolbox.h"
+#include "../HttpServer/HttpHandler.h"
+
 #include <map>
 
 namespace Orthanc
@@ -45,19 +47,34 @@
     std::vector<std::string> components_;
 
   public:
-    typedef std::map<std::string, std::string> Components;
-
     RestApiPath(const std::string& uri);
 
     // This version is slower
-    bool Match(Components& components,
+    bool Match(HttpHandler::Arguments& components,
                UriComponents& trailing,
                const std::string& uriRaw) const;
 
-    bool Match(Components& components,
+    bool Match(HttpHandler::Arguments& components,
                UriComponents& trailing,
                const UriComponents& uri) const;
 
     bool Match(const UriComponents& uri) const;
+
+    size_t GetLevelCount() const
+    {
+      return uri_.size();
+    }
+
+    bool IsWildcardLevel(size_t level) const;
+
+    bool IsUniversalTrailing() const
+    {
+      return hasTrailing_;
+    }
+
+    const std::string& GetWildcardName(size_t level) const;
+
+    const std::string& GetLevelName(size_t level) const;
+
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiPostCall.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "RestApiCall.h"
+
+namespace Orthanc
+{
+  class RestApiPostCall : public RestApiCall
+  {
+  private:
+    const std::string& data_;
+
+  public:
+    typedef void (*Handler) (RestApiPostCall& call);
+    
+    RestApiPostCall(RestApiOutput& output,
+                    RestApi& context,
+                    const HttpHandler::Arguments& httpHeaders,
+                    const HttpHandler::Arguments& uriComponents,
+                    const UriComponents& trailing,
+                    const UriComponents& fullUri,
+                    const std::string& data) :
+      RestApiCall(output, context, httpHeaders, uriComponents, trailing, fullUri),
+      data_(data)
+    {
+    }
+
+    const std::string& GetPostBody() const
+    {
+      return data_;
+    }
+
+    virtual bool ParseJsonRequest(Json::Value& result) const
+    {
+      return ParseJsonRequestInternal(result, GetPostBody().c_str());
+    }      
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiPutCall.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "RestApiCall.h"
+
+namespace Orthanc
+{
+  class RestApiPutCall : public RestApiCall
+  {
+  private:
+    const std::string& data_;
+
+  public:
+    typedef void (*Handler) (RestApiPutCall& call);
+    
+    RestApiPutCall(RestApiOutput& output,
+                   RestApi& context,
+                   const HttpHandler::Arguments& httpHeaders,
+                   const HttpHandler::Arguments& uriComponents,
+                   const UriComponents& trailing,
+                   const UriComponents& fullUri,
+                   const std::string& data) :
+      RestApiCall(output, context, httpHeaders, uriComponents, trailing, fullUri),
+      data_(data)
+    {
+    }
+
+    const std::string& GetPutBody() const
+    {
+      return data_;
+    }
+
+    virtual bool ParseJsonRequest(Json::Value& result) const
+    {
+      return ParseJsonRequestInternal(result, GetPutBody().c_str());
+    }      
+  };
+}
--- a/Core/SQLite/Connection.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/Connection.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -34,15 +35,21 @@
  **/
 
 
+#if ORTHANC_SQLITE_STANDALONE != 1
 #include "../PrecompiledHeaders.h"
+#endif
+
 #include "Connection.h"
+#include "OrthancSQLiteException.h"
 
 #include <memory>
 #include <cassert>
 #include <sqlite3.h>
 #include <string.h>
 
+#if ORTHANC_SQLITE_STANDALONE != 1
 #include <glog/logging.h>
+#endif
 
 
 namespace Orthanc
@@ -67,7 +74,7 @@
     {
       if (!db_)
       {
-        throw OrthancException("SQLite: The database is not opened");
+        throw OrthancSQLiteException("SQLite: The database is not opened");
       }
     }
 
@@ -75,7 +82,7 @@
     {
       if (db_) 
       {
-        throw OrthancException("SQLite: Connection is already open");
+        throw OrthancSQLiteException("SQLite: Connection is already open");
       }
 
       int err = sqlite3_open(path.c_str(), &db_);
@@ -83,7 +90,7 @@
       {
         Close();
         db_ = NULL;
-        throw OrthancException("SQLite: Unable to open the database");
+        throw OrthancSQLiteException("SQLite: Unable to open the database");
       }
 
       // Execute PRAGMAs at this point
@@ -129,7 +136,7 @@
       {
         if (i->second->GetReferenceCount() >= 1)
         {
-          throw OrthancException("SQLite: This cached statement is already being referred to");
+          throw OrthancSQLiteException("SQLite: This cached statement is already being referred to");
         }
 
         return *i->second;
@@ -145,13 +152,16 @@
 
     bool Connection::Execute(const char* sql) 
     {
+#if ORTHANC_SQLITE_STANDALONE != 1
       VLOG(1) << "SQLite::Connection::Execute " << sql;
+#endif
+
       CheckIsOpen();
 
       int error = sqlite3_exec(db_, sql, NULL, NULL, NULL);
       if (error == SQLITE_ERROR)
       {
-        throw OrthancException("SQLite Execute error: " + std::string(sqlite3_errmsg(db_)));
+        throw OrthancSQLiteException("SQLite Execute error: " + std::string(sqlite3_errmsg(db_)));
       }
       else
       {
@@ -272,7 +282,7 @@
     {
       if (!transactionNesting_)
       {
-        throw OrthancException("Rolling back a nonexistent transaction");
+        throw OrthancSQLiteException("Rolling back a nonexistent transaction");
       }
 
       transactionNesting_--;
@@ -291,7 +301,7 @@
     {
       if (!transactionNesting_) 
       {
-        throw OrthancException("Committing a nonexistent transaction");
+        throw OrthancSQLiteException("Committing a nonexistent transaction");
       }
       transactionNesting_--;
 
@@ -359,7 +369,7 @@
       if (err != SQLITE_OK)
       {
         delete func;
-        throw OrthancException("SQLite: Unable to register a function");
+        throw OrthancSQLiteException("SQLite: Unable to register a function");
       }
 
       return func;
@@ -368,12 +378,15 @@
 
     void Connection::FlushToDisk()
     {
+#if ORTHANC_SQLITE_STANDALONE != 1
       VLOG(1) << "SQLite::Connection::FlushToDisk";
+#endif
+
       int err = sqlite3_wal_checkpoint(db_, NULL);
 
       if (err != SQLITE_OK)
       {
-        throw OrthancException("SQLite: Unable to flush the database");
+        throw OrthancSQLiteException("SQLite: Unable to flush the database");
       }
     }
   }
--- a/Core/SQLite/Connection.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/Connection.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -40,7 +41,6 @@
 #include "IScalarFunction.h"
 
 #include <string>
-#include <boost/noncopyable.hpp>
 #include <map>
 
 struct sqlite3;
@@ -52,7 +52,7 @@
 {
   namespace SQLite
   {
-    class Connection : boost::noncopyable
+    class Connection : NonCopyable
     {
       friend class Statement;
       friend class Transaction;
--- a/Core/SQLite/FunctionContext.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/FunctionContext.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
@@ -31,8 +32,12 @@
  **/
 
 
+#if ORTHANC_SQLITE_STANDALONE != 1
 #include "../PrecompiledHeaders.h"
+#endif
+
 #include "FunctionContext.h"
+#include "OrthancSQLiteException.h"
 
 #include <sqlite3.h>
 
@@ -57,7 +62,7 @@
     {
       if (index >= argc_)
       {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancSQLiteException("Parameter out of range");
       }
     }
 
--- a/Core/SQLite/FunctionContext.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/FunctionContext.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
@@ -33,8 +34,6 @@
 
 #pragma once
 
-#include <boost/noncopyable.hpp>
-
 #include "Statement.h"
 
 struct sqlite3_context;
@@ -44,7 +43,7 @@
 {
   namespace SQLite
   {
-    class FunctionContext : public boost::noncopyable
+    class FunctionContext : public NonCopyable
     {
       friend class Connection;
 
--- a/Core/SQLite/IScalarFunction.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/IScalarFunction.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
@@ -33,13 +34,14 @@
 
 #pragma once
 
+#include "NonCopyable.h"
 #include "FunctionContext.h"
 
 namespace Orthanc
 {
   namespace SQLite
   {
-    class IScalarFunction : public boost::noncopyable
+    class IScalarFunction : public NonCopyable
     {
     public:
       virtual ~IScalarFunction()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SQLite/ITransaction.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,66 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ * nor the names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+
+#pragma once
+
+#include "NonCopyable.h"
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class ITransaction : public NonCopyable
+    {
+    public:
+      virtual ~ITransaction()
+      {
+      }
+
+      // Begins the transaction. This uses the default sqlite "deferred" transaction
+      // type, which means that the DB lock is lazily acquired the next time the
+      // database is accessed, not in the begin transaction command.
+      virtual void Begin() = 0;
+
+      // Rolls back the transaction. This will happen automatically if you do
+      // nothing when the transaction goes out of scope.
+      virtual void Rollback() = 0;
+
+      // Commits the transaction, returning true on success.
+      virtual void Commit() = 0;
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SQLite/NonCopyable.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,60 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ * nor the names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+
+#pragma once
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    // This class mimics "boost::noncopyable"
+    class NonCopyable
+    {
+    private:
+      NonCopyable(const NonCopyable&);
+
+      NonCopyable& operator= (const NonCopyable&);
+
+    protected:
+      NonCopyable()
+      {
+      }
+
+      ~NonCopyable()
+      {
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SQLite/OrthancSQLiteException.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,67 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *    * Neither the name of Google Inc., the name of the CHU of Liege,
+ * nor the names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+
+#pragma once
+
+
+#if ORTHANC_SQLITE_STANDALONE == 1
+#include <stdexcept>
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class OrthancSQLiteException : public ::std::runtime_error
+    {
+    public:
+      OrthancSQLiteException(const std::string& what) :
+        ::std::runtime_error(what)
+      {
+      }
+
+      OrthancSQLiteException(const char* what) : 
+        ::std::runtime_error(what)
+      {
+      }
+    };
+  }
+}
+
+#else
+#  include "../OrthancException.h"
+#  define OrthancSQLiteException ::Orthanc::OrthancException
+#endif
--- a/Core/SQLite/README.txt	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/README.txt	Thu May 21 16:58:30 2015 +0200
@@ -19,6 +19,17 @@
   coding conventions.
 
 
+Reuse in another software
+=========================
+
+To use the Orthanc SQLite wrapper in another project than Orthanc, you
+just have to define the "ORTHANC_SQLITE_STANDALONE" macro.
+
+All the C++ exceptions generated by the wrapper will be objects of the
+class "::Orthanc::SQLite::OrthancSQLiteException", that derives from
+the standard exception class "::std::runtime_error".
+
+
 Licensing
 =========
 
@@ -26,4 +37,4 @@
 order to respect the original license of the code.
 
 It is pretty straightforward to extract the code from this folder and
-to include it in another project.
+to include it in another project. 
--- a/Core/SQLite/Statement.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/Statement.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -34,16 +35,25 @@
  **/
 
 
+#if ORTHANC_SQLITE_STANDALONE != 1
 #include "../PrecompiledHeaders.h"
+#endif
+
 #include "Statement.h"
-
 #include "Connection.h"
-#include "../Toolbox.h"
 
-#include <boost/lexical_cast.hpp>
 #include <sqlite3.h>
 #include <string.h>
+#include <stdio.h>
+#include <algorithm>
+
+#if ORTHANC_SQLITE_STANDALONE != 1
 #include <glog/logging.h>
+#endif
+
+#if defined(_MSC_VER)
+#define snprintf _snprintf
+#endif
 
 namespace Orthanc
 {
@@ -54,7 +64,9 @@
       bool succeeded = (err == SQLITE_OK || err == SQLITE_ROW || err == SQLITE_DONE);
       if (!succeeded)
       {
-        throw OrthancException("SQLite error code " + boost::lexical_cast<std::string>(err));
+        char buffer[128];
+        snprintf(buffer, sizeof(buffer) - 1, "SQLite error code %d", err);
+        throw OrthancSQLiteException(buffer);
       }
 
       return err;
@@ -65,11 +77,13 @@
       if (err == SQLITE_RANGE)
       {
         // Binding to a non-existent variable is evidence of a serious error.
-        throw OrthancException("Bind value out of range");
+        throw OrthancSQLiteException("Bind value out of range");
       }
       else if (err != SQLITE_OK)
       {
-        throw OrthancException("SQLite error code " + boost::lexical_cast<std::string>(err));
+        char buffer[128];
+        snprintf(buffer, sizeof(buffer) - 1, "SQLite error code %d", err);
+        throw OrthancSQLiteException(buffer);
       }
     }
 
@@ -108,13 +122,19 @@
 
     bool Statement::Run()
     {
+#if ORTHANC_SQLITE_STANDALONE != 1
       VLOG(1) << "SQLite::Statement::Run " << sqlite3_sql(GetStatement());
+#endif
+
       return CheckError(sqlite3_step(GetStatement())) == SQLITE_DONE;
     }
 
     bool Statement::Step()
     {
+#if ORTHANC_SQLITE_STANDALONE != 1
       VLOG(1) << "SQLite::Statement::Step " << sqlite3_sql(GetStatement());
+#endif
+
       return CheckError(sqlite3_step(GetStatement())) == SQLITE_ROW;
     }
 
@@ -206,7 +226,7 @@
     ColumnType Statement::GetDeclaredColumnType(int col) const 
     {
       std::string column_type(sqlite3_column_decltype(GetStatement(), col));
-      Toolbox::ToLowerCase(column_type);
+      std::transform(column_type.begin(), column_type.end(), column_type.begin(), tolower);
 
       if (column_type == "integer")
         return COLUMN_TYPE_INTEGER;
--- a/Core/SQLite/Statement.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/Statement.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -36,13 +37,13 @@
 
 #pragma once
 
-#include "../OrthancException.h"
+#include "NonCopyable.h"
+#include "OrthancSQLiteException.h"
 #include "StatementId.h"
 #include "StatementReference.h"
 
 #include <vector>
 #include <stdint.h>
-#include <boost/noncopyable.hpp>
 
 #if ORTHANC_BUILD_UNIT_TESTS == 1
 #include <gtest/gtest_prod.h>
@@ -68,7 +69,7 @@
       COLUMN_TYPE_NULL = 5
     };
 
-    class Statement : public boost::noncopyable
+    class Statement : public NonCopyable
     {
       friend class Connection;
 
--- a/Core/SQLite/StatementId.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/StatementId.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -34,7 +35,10 @@
  **/
 
 
+#if ORTHANC_SQLITE_STANDALONE != 1
 #include "../PrecompiledHeaders.h"
+#endif
+
 #include "StatementId.h"
 
 #include <string.h>
--- a/Core/SQLite/StatementId.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/StatementId.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
--- a/Core/SQLite/StatementReference.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/StatementReference.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -34,13 +35,18 @@
  **/
 
 
+#if ORTHANC_SQLITE_STANDALONE != 1
 #include "../PrecompiledHeaders.h"
+#endif
+
 #include "StatementReference.h"
+#include "OrthancSQLiteException.h"
 
-#include "../OrthancException.h"
+#if ORTHANC_SQLITE_STANDALONE != 1
+#include <glog/logging.h>
+#endif
 
 #include <cassert>
-#include <glog/logging.h>
 #include "sqlite3.h"
 
 namespace Orthanc
@@ -65,7 +71,7 @@
     {
       if (database == NULL || sql == NULL)
       {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancSQLiteException("Parameter out of range");
       }
 
       root_ = NULL;
@@ -74,7 +80,7 @@
       int error = sqlite3_prepare_v2(database, sql, -1, &statement_, NULL);
       if (error != SQLITE_OK)
       {
-        throw OrthancException("SQLite: " + std::string(sqlite3_errmsg(database)));
+        throw OrthancSQLiteException("SQLite: " + std::string(sqlite3_errmsg(database)));
       }
 
       assert(IsRoot());
@@ -109,7 +115,9 @@
           // an exception because:
           // http://www.parashift.com/c++-faq/dtors-shouldnt-throw.html
 
+#if ORTHANC_SQLITE_STANDALONE != 1
           LOG(ERROR) << "Bad value of the reference counter";
+#endif
         }
         else if (statement_ != NULL)
         {
@@ -124,7 +132,9 @@
           // an exception because:
           // http://www.parashift.com/c++-faq/dtors-shouldnt-throw.html
 
+#if ORTHANC_SQLITE_STANDALONE != 1
           LOG(ERROR) << "Bad value of the reference counter";
+#endif
         }
         else
         {
--- a/Core/SQLite/StatementReference.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/StatementReference.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -36,7 +37,8 @@
 
 #pragma once
 
-#include <boost/noncopyable.hpp>
+#include "NonCopyable.h"
+
 #include <stdint.h>
 #include <cassert>
 #include <stdlib.h>
@@ -48,7 +50,7 @@
 {
   namespace SQLite
   {
-    class StatementReference : boost::noncopyable
+    class StatementReference : NonCopyable
     {
     private:
       StatementReference* root_;   // Only used for non-root nodes
--- a/Core/SQLite/Transaction.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/Transaction.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -34,8 +35,12 @@
  **/
 
 
+#if ORTHANC_SQLITE_STANDALONE != 1
 #include "../PrecompiledHeaders.h"
+#endif
+
 #include "Transaction.h"
+#include "OrthancSQLiteException.h"
 
 namespace Orthanc
 {
@@ -59,13 +64,13 @@
     {
       if (isOpen_) 
       {
-        throw OrthancException("SQLite: Beginning a transaction twice!");
+        throw OrthancSQLiteException("SQLite: Beginning a transaction twice!");
       }
 
       isOpen_ = connection_.BeginTransaction();
       if (!isOpen_)
       {
-        throw OrthancException("SQLite: Unable to create a transaction");
+        throw OrthancSQLiteException("SQLite: Unable to create a transaction");
       }
     }
 
@@ -73,8 +78,8 @@
     {
       if (!isOpen_) 
       {
-        throw OrthancException("SQLite: Attempting to roll back a nonexistent transaction. "
-                                "Did you remember to call Begin()?");
+        throw OrthancSQLiteException("SQLite: Attempting to roll back a nonexistent transaction. "
+                                     "Did you remember to call Begin()?");
       }
 
       isOpen_ = false;
@@ -86,15 +91,15 @@
     {
       if (!isOpen_) 
       {
-        throw OrthancException("SQLite: Attempting to roll back a nonexistent transaction. "
-                                "Did you remember to call Begin()?");
+        throw OrthancSQLiteException("SQLite: Attempting to roll back a nonexistent transaction. "
+                                     "Did you remember to call Begin()?");
       }
 
       isOpen_ = false;
 
       if (!connection_.CommitTransaction())
       {
-        throw OrthancException("SQLite: Failure when committing the transaction");
+        throw OrthancSQLiteException("SQLite: Failure when committing the transaction");
       }
     }
   }
--- a/Core/SQLite/Transaction.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/SQLite/Transaction.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,8 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ *
+ * Copyright (C) 2012-2015 Sebastien Jodogne <s.jodogne@gmail.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
  *
  * Copyright (c) 2012 The Chromium Authors. All rights reserved.
  *
@@ -37,12 +38,13 @@
 #pragma once
 
 #include "Connection.h"
+#include "ITransaction.h"
 
 namespace Orthanc
 {
   namespace SQLite
   {
-    class Transaction : public boost::noncopyable
+    class Transaction : public ITransaction
     {
     private:
       Connection& connection_;
@@ -53,22 +55,17 @@
 
     public:
       explicit Transaction(Connection& connection);
-      ~Transaction();
+
+      virtual ~Transaction();
 
       // Returns true when there is a transaction that has been successfully begun.
       bool IsOpen() const { return isOpen_; }
 
-      // Begins the transaction. This uses the default sqlite "deferred" transaction
-      // type, which means that the DB lock is lazily acquired the next time the
-      // database is accessed, not in the begin transaction command.
-      void Begin();
+      virtual void Begin();
 
-      // Rolls back the transaction. This will happen automatically if you do
-      // nothing when the transaction goes out of scope.
-      void Rollback();
+      virtual void Rollback();
 
-      // Commits the transaction, returning true on success.
-      void Commit();
+      virtual void Commit();
     };
   }
 }
--- a/Core/Toolbox.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Toolbox.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -44,9 +44,14 @@
 #include <algorithm>
 #include <ctype.h>
 #include <boost/regex.hpp> 
+#include <glog/logging.h>
 
 #if defined(_WIN32)
 #include <windows.h>
+#include <process.h>   // For "_spawnvp()"
+#else
+#include <unistd.h>    // For "execvp()"
+#include <sys/wait.h>  // For "waitpid()"
 #endif
 
 #if defined(__APPLE__) && defined(__MACH__)
@@ -54,7 +59,7 @@
 #include <limits.h>      /* PATH_MAX */
 #endif
 
-#if defined(__linux) || defined(__FreeBSD_kernel__)
+#if defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
 #include <limits.h>      /* PATH_MAX */
 #include <signal.h>
 #include <unistd.h>
@@ -70,8 +75,8 @@
 #include "../Resources/ThirdParty/base64/base64.h"
 
 
-#ifdef _MSC_VER
-// Patch for the missing "_strtoll" symbol when compiling with Visual Studio
+#if defined(_MSC_VER) && (_MSC_VER < 1800)
+// Patch for the missing "_strtoll" symbol when compiling with Visual Studio < 2013
 extern "C"
 {
   int64_t _strtoi64(const char *nptr, char **endptr, int base);
@@ -83,6 +88,12 @@
 #endif
 
 
+#if ORTHANC_PUGIXML_ENABLED == 1
+#include "ChunkedBuffer.h"
+#include <pugixml.hpp>
+#endif
+
+
 namespace Orthanc
 {
   static bool finish;
@@ -105,7 +116,7 @@
   {
 #if defined(_WIN32)
     ::Sleep(static_cast<DWORD>(microSeconds / static_cast<uint64_t>(1000)));
-#elif defined(__linux) || defined(__APPLE__) || defined(__FreeBSD_kernel__)
+#elif defined(__linux) || defined(__APPLE__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
     usleep(microSeconds);
 #else
 #error Support your platform here
@@ -113,7 +124,7 @@
   }
 
 
-  void Toolbox::ServerBarrier()
+  static void ServerBarrierInternal(const bool* stopFlag)
   {
 #if defined(_WIN32)
     SetConsoleCtrlHandler(ConsoleControlHandler, true);
@@ -123,10 +134,11 @@
     signal(SIGTERM, SignalHandler);
 #endif
   
+    // Active loop that awakens every 100ms
     finish = false;
-    while (!finish)
+    while (!(*stopFlag || finish))
     {
-      USleep(100000);
+      Toolbox::USleep(100 * 1000);
     }
 
 #if defined(_WIN32)
@@ -139,6 +151,17 @@
   }
 
 
+  void Toolbox::ServerBarrier(const bool& stopFlag)
+  {
+    ServerBarrierInternal(&stopFlag);
+  }
+
+  void Toolbox::ServerBarrier()
+  {
+    const bool stopFlag = false;
+    ServerBarrierInternal(&stopFlag);
+  }
+
 
   void Toolbox::ToUpperCase(std::string& s)
   {
@@ -284,6 +307,28 @@
   }
 
 
+  void Toolbox::TruncateUri(UriComponents& target,
+                            const UriComponents& source,
+                            size_t fromLevel)
+  {
+    target.clear();
+
+    if (source.size() > fromLevel)
+    {
+      target.resize(source.size() - fromLevel);
+
+      size_t j = 0;
+      for (size_t i = fromLevel; i < source.size(); i++, j++)
+      {
+        target[j] = source[i];
+      }
+
+      assert(j == target.size());
+    }
+  }
+  
+
+
   bool Toolbox::IsChildUri(const UriComponents& baseUri,
                            const UriComponents& testedUri)
   {
@@ -449,7 +494,7 @@
 
 
 #if defined(_WIN32)
-  std::string Toolbox::GetPathToExecutable()
+  static std::string GetPathToExecutableInternal()
   {
     // Yes, this is ugly, but there is no simple way to get the 
     // required buffer size, so we use a big constant
@@ -458,8 +503,8 @@
     return std::string(&buffer[0]);
   }
 
-#elif defined(__linux) || defined(__FreeBSD_kernel__)
-  std::string Toolbox::GetPathToExecutable()
+#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+  static std::string GetPathToExecutableInternal()
   {
     std::vector<char> buffer(PATH_MAX + 1);
     ssize_t bytes = readlink("/proc/self/exe", &buffer[0], buffer.size() - 1);
@@ -472,7 +517,7 @@
   }
 
 #elif defined(__APPLE__) && defined(__MACH__)
-  std::string Toolbox::GetPathToExecutable()
+  static std::string GetPathToExecutableInternal()
   {
     char pathbuf[PATH_MAX + 1];
     unsigned int  bufsize = static_cast<int>(sizeof(pathbuf));
@@ -487,10 +532,17 @@
 #endif
 
 
+  std::string Toolbox::GetPathToExecutable()
+  {
+    boost::filesystem::path p(GetPathToExecutableInternal());
+    return boost::filesystem::absolute(p).string();
+  }
+
+
   std::string Toolbox::GetDirectoryOfExecutable()
   {
-    boost::filesystem::path p(GetPathToExecutable());
-    return p.parent_path().string();
+    boost::filesystem::path p(GetPathToExecutableInternal());
+    return boost::filesystem::absolute(p.parent_path()).string();
   }
 
 
@@ -499,18 +551,71 @@
   {
     const char* encoding;
 
+
+    // http://bradleyross.users.sourceforge.net/docs/dicom/doc/src-html/org/dcm4che2/data/SpecificCharacterSet.html
     switch (sourceEncoding)
     {
       case Encoding_Utf8:
         // Already in UTF-8: No conversion is required
         return source;
 
+      case Encoding_Ascii:
+        return ConvertToAscii(source);
+
       case Encoding_Latin1:
         encoding = "ISO-8859-1";
         break;
 
+      case Encoding_Latin2:
+        encoding = "ISO-8859-2";
+        break;
+
+      case Encoding_Latin3:
+        encoding = "ISO-8859-3";
+        break;
+
+      case Encoding_Latin4:
+        encoding = "ISO-8859-4";
+        break;
+
+      case Encoding_Latin5:
+        encoding = "ISO-8859-9";
+        break;
+
+      case Encoding_Cyrillic:
+        encoding = "ISO-8859-5";
+        break;
+
+      case Encoding_Windows1251:
+        encoding = "WINDOWS-1251";
+        break;
+
+      case Encoding_Arabic:
+        encoding = "ISO-8859-6";
+        break;
+
+      case Encoding_Greek:
+        encoding = "ISO-8859-7";
+        break;
+
+      case Encoding_Hebrew:
+        encoding = "ISO-8859-8";
+        break;
+        
+      case Encoding_Japanese:
+        encoding = "SHIFT-JIS";
+        break;
+
+      case Encoding_Chinese:
+        encoding = "GB18030";
+        break;
+
+      case Encoding_Thai:
+        encoding = "TIS620.2533-0";
+        break;
+
       default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancException(ErrorCode_NotImplemented);
     }
 
     try
@@ -532,7 +637,7 @@
     result.reserve(source.size() + 1);
     for (size_t i = 0; i < source.size(); i++)
     {
-      if (source[i] < 128 && source[i] >= 0 && !iscntrl(source[i]))
+      if (source[i] <= 127 && source[i] >= 0 && !iscntrl(source[i]))
       {
         result.push_back(source[i]);
       }
@@ -792,5 +897,220 @@
       }
     }
   }
+
+
+  bool Toolbox::IsExistingFile(const std::string& path)
+  {
+    return boost::filesystem::exists(path);
+  }
+
+
+#if ORTHANC_PUGIXML_ENABLED == 1
+  class ChunkedBufferWriter : public pugi::xml_writer
+  {
+  private:
+    ChunkedBuffer buffer_;
+
+  public:
+    virtual void write(const void *data, size_t size)
+    {
+      if (size > 0)
+      {
+        buffer_.AddChunk(reinterpret_cast<const char*>(data), size);
+      }
+    }
+
+    void Flatten(std::string& s)
+    {
+      buffer_.Flatten(s);
+    }
+  };
+
+
+  static void JsonToXmlInternal(pugi::xml_node& target,
+                                const Json::Value& source,
+                                const std::string& arrayElement)
+  {
+    // http://jsoncpp.sourceforge.net/value_8h_source.html#l00030
+
+    switch (source.type())
+    {
+      case Json::nullValue:
+      {
+        target.append_child(pugi::node_pcdata).set_value("null");
+        break;
+      }
+
+      case Json::intValue:
+      {
+        std::string s = boost::lexical_cast<std::string>(source.asInt());
+        target.append_child(pugi::node_pcdata).set_value(s.c_str());
+        break;
+      }
+
+      case Json::uintValue:
+      {
+        std::string s = boost::lexical_cast<std::string>(source.asUInt());
+        target.append_child(pugi::node_pcdata).set_value(s.c_str());
+        break;
+      }
+
+      case Json::realValue:
+      {
+        std::string s = boost::lexical_cast<std::string>(source.asFloat());
+        target.append_child(pugi::node_pcdata).set_value(s.c_str());
+        break;
+      }
+
+      case Json::stringValue:
+      {
+        target.append_child(pugi::node_pcdata).set_value(source.asString().c_str());
+        break;
+      }
+
+      case Json::booleanValue:
+      {
+        target.append_child(pugi::node_pcdata).set_value(source.asBool() ? "true" : "false");
+        break;
+      }
+
+      case Json::arrayValue:
+      {
+        for (Json::Value::ArrayIndex i = 0; i < source.size(); i++)
+        {
+          pugi::xml_node node = target.append_child();
+          node.set_name(arrayElement.c_str());
+          JsonToXmlInternal(node, source[i], arrayElement);
+        }
+        break;
+      }
+        
+      case Json::objectValue:
+      {
+        Json::Value::Members members = source.getMemberNames();
+
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          pugi::xml_node node = target.append_child();
+          node.set_name(members[i].c_str());
+          JsonToXmlInternal(node, source[members[i]], arrayElement);          
+        }
+
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void Toolbox::JsonToXml(std::string& target,
+                          const Json::Value& source,
+                          const std::string& rootElement,
+                          const std::string& arrayElement)
+  {
+    pugi::xml_document doc;
+
+    pugi::xml_node n = doc.append_child(rootElement.c_str());
+    JsonToXmlInternal(n, source, arrayElement);
+
+    pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
+    decl.append_attribute("version").set_value("1.0");
+    decl.append_attribute("encoding").set_value("utf-8");
+
+    ChunkedBufferWriter writer;
+    doc.save(writer, "  ", pugi::format_default, pugi::encoding_utf8);
+    writer.Flatten(target);
+  }
+
+#endif
+
+
+  void Toolbox::ExecuteSystemCommand(const std::string& command,
+                                     const std::vector<std::string>& arguments)
+  {
+    // Convert the arguments as a C array
+    std::vector<char*>  args(arguments.size() + 2);
+
+    args.front() = const_cast<char*>(command.c_str());
+
+    for (size_t i = 0; i < arguments.size(); i++)
+    {
+      args[i + 1] = const_cast<char*>(arguments[i].c_str());
+    }
+
+    args.back() = NULL;
+
+    int status;
+
+#if defined(_WIN32)
+    // http://msdn.microsoft.com/en-us/library/275khfab.aspx
+    status = static_cast<int>(_spawnvp(_P_OVERLAY, command.c_str(), &args[0]));
+
+#else
+    int pid = fork();
+
+    if (pid == -1)
+    {
+      // Error in fork()
+      LOG(ERROR) << "Cannot fork a child process";
+      throw OrthancException(ErrorCode_SystemCommand);
+    }
+    else if (pid == 0)
+    {
+      // Execute the system command in the child process
+      execvp(command.c_str(), &args[0]);
+
+      // We should never get here
+      _exit(1);
+    }
+    else
+    {
+      // Wait for the system command to exit
+      waitpid(pid, &status, 0);
+    }
+#endif
+
+    if (status != 0)
+    {
+      LOG(ERROR) << "System command failed with status code " << status;
+      throw OrthancException(ErrorCode_SystemCommand);
+    }
+  }
+
+  
+  bool Toolbox::IsInteger(const std::string& str)
+  {
+    std::string s = StripSpaces(str);
+
+    if (s.size() == 0)
+    {
+      return false;
+    }
+
+    size_t pos = 0;
+    if (s[0] == '-')
+    {
+      if (s.size() == 1)
+      {
+        return false;
+      }
+
+      pos = 1;
+    }
+
+    while (pos < s.size())
+    {
+      if (!isdigit(s[pos]))
+      {
+        return false;
+      }
+
+      pos++;
+    }
+
+    return true;
+  }
 }
 
--- a/Core/Toolbox.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Toolbox.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -38,6 +38,10 @@
 #include <vector>
 #include <string>
 
+#if ORTHANC_PUGIXML_ENABLED == 1
+#include <json/json.h>
+#endif
+
 namespace Orthanc
 {
   typedef std::vector<std::string> UriComponents;
@@ -48,6 +52,8 @@
 
   namespace Toolbox
   {
+    void ServerBarrier(const bool& stopFlag);
+
     void ServerBarrier();
 
     void ToUpperCase(std::string& s);  // Inplace version
@@ -73,6 +79,10 @@
     void SplitUriComponents(UriComponents& components,
                             const std::string& uri);
   
+    void TruncateUri(UriComponents& target,
+                     const UriComponents& source,
+                     size_t fromLevel);
+  
     bool IsChildUri(const UriComponents& baseUri,
                     const UriComponents& testedUri);
 
@@ -130,5 +140,19 @@
                              const std::string& source);
 
     void CreateDirectory(const std::string& path);
+
+    bool IsExistingFile(const std::string& path);
+
+#if ORTHANC_PUGIXML_ENABLED == 1
+    void JsonToXml(std::string& target,
+                   const Json::Value& source,
+                   const std::string& rootElement = "root",
+                   const std::string& arrayElement = "item");
+#endif
+
+    void ExecuteSystemCommand(const std::string& command,
+                              const std::vector<std::string>& arguments);
+
+    bool IsInteger(const std::string& str);
   }
 }
--- a/Core/Uuid.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Uuid.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Core/Uuid.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/Core/Uuid.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -43,6 +43,8 @@
  * http://stackoverflow.com/questions/246930/is-there-any-difference-between-a-guid-and-a-uuid
  **/
 
+#include "Toolbox.h"
+
 namespace Orthanc
 {
   namespace Toolbox
@@ -69,6 +71,16 @@
       {
         return path_;
       }
+
+      void Write(const std::string& content)
+      {
+        Toolbox::WriteFile(content, path_);
+      }
+
+      void Read(std::string& content) const
+      {
+        Toolbox::ReadFile(content, path_);
+      }
     };
   }
 }
--- a/DarwinCompilation.txt	Wed Jun 25 15:34:40 2014 +0200
+++ b/DarwinCompilation.txt	Thu May 21 16:58:30 2015 +0200
@@ -28,7 +28,7 @@
 ----------------------------
 
 # cd ~/OrthancBuild
-# cmake -GXcode -DCMAKE_OSX_DEPLOYMENT_TARGET=10.8 -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON ..
+# cmake -GXcode -DCMAKE_OSX_DEPLOYMENT_TARGET=10.8 -DSTATIC_BUILD=ON -DSTANDALONE_BUILD=ON -DALLOW_DOWNLOADS=ON ~/Orthanc
 
 NB: Adapt the value of "CMAKE_OSX_DEPLOYMENT_TARGET" with respect to
 your version of XCode.
--- a/INSTALL	Wed Jun 25 15:34:40 2014 +0200
+++ b/INSTALL	Thu May 21 16:58:30 2015 +0200
@@ -54,16 +54,18 @@
 
 
 
-Native Windows build with Microsoft Visual Studio 2005
-------------------------------------------------------
+Native Windows build with Microsoft Visual Studio
+-------------------------------------------------
 
 # cd [...]\OrthancBuild
-# cmake -DSTANDALONE_BUILD=ON -G "Visual Studio 8 2005" [...]\Orthanc
+# cmake -DSTANDALONE_BUILD=ON -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON -G "Visual Studio 8 2005" [...]\Orthanc
 
 Then open the "[...]/OrthancBuild/Orthanc.sln" with Visual Studio.
 
 NOTES:
-* More recent versions of Visual Studio should also work.
+* More recent versions of Visual Studio than 2005 should also
+  work. Type "cmake" without arguments to have the list of generators
+  that are available on your computer.
 * You will have to install the Platform SDK (version 6 or above) for
   Visual Studio 2005:
   http://en.wikipedia.org/wiki/Microsoft_Windows_SDK.
--- a/LinuxCompilation.txt	Wed Jun 25 15:34:40 2014 +0200
+++ b/LinuxCompilation.txt	Thu May 21 16:58:30 2015 +0200
@@ -9,15 +9,17 @@
 statically linking against all the third-party dependencies. In this
 case, the system-wide libraries will not be used. The build tool
 (CMake) will download the sources of all the required packages and
-automatically compile them. This process should work on all the Linux
-distributions.
+automatically compile them.
 
-We make the assumption that Orthanc source code is placed in the
-folder "~/Orthanc" and that the binaries will be compiled to
-"~/OrthancBuild".
+This process should work on any Linux distribution, provided that a
+C/C++ compiler ("build-essential" in Debian-based systems), the Python
+interpreter, CMake, the "unzip" system tool, and the development
+package for libuuid ("uuid-dev" in Debian) are installed.
 
 
-To build binaries with debug information:
+We now make the assumption that Orthanc source code is placed in the
+folder "~/Orthanc" and that the binaries will be compiled to
+"~/OrthancBuild". To build binaries with debug information:
 
 # cd ~/OrthancBuild
 # cmake -DSTATIC_BUILD=ON -DCMAKE_BUILD_TYPE=Debug ~/Orthanc
@@ -33,10 +35,17 @@
 # make doc
 
 
-Note: When the "STATIC_BUILD" option is set to "ON", the build tool
+Note 1- When the "STATIC_BUILD" option is set to "ON", the build tool
 will not ask you the permission to download packages from the
 Internet.
 
+Note 2- If the development package of libuuid was not installed when
+first invoking cmake, you will have to manually remove the build
+directory ("rm -rf ~/OrthancBuild") after installing this package,
+then run cmake again.
+
+Note 3- To build the documentation, you will have to install doxyen.
+
 
 Use system-wide libraries under Linux
 =====================================
@@ -51,6 +60,7 @@
 # cmake -DCMAKE_BUILD_TYPE=Debug ~/Orthanc
 # make
 
+Note that to build the documentation, you will have to install doxyen.
 
 However, on some Linux distributions, it is still required to download
 and static link against some third-party dependencies, e.g. when the
@@ -77,6 +87,7 @@
 	-DUSE_SYSTEM_DCMTK=OFF \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
 	-DUSE_SYSTEM_JSONCPP=OFF \
+	-DUSE_SYSTEM_PUGIXML=OFF \
         -DENABLE_JPEG=OFF \
         -DENABLE_JPEG_LOSSLESS=OFF \
 	~/Orthanc 
@@ -95,6 +106,7 @@
         -DUSE_SYSTEM_GOOGLE_LOG=OFF \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
         -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+	-DUSE_SYSTEM_PUGIXML=OFF \
         -DENABLE_JPEG=OFF \
         -DENABLE_JPEG_LOSSLESS=OFF \
 	~/Orthanc
@@ -107,35 +119,34 @@
        	       	       uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \
        	       	       libgoogle-glog-dev libgtest-dev libpng-dev \
        	       	       libsqlite3-dev libssl-dev zlib1g-dev libdcmtk2-dev \
-                       libboost-all-dev libwrap0-dev libjsoncpp-dev
+                       libboost-all-dev libwrap0-dev libjsoncpp-dev libpugixml-dev
 
 # cmake -DALLOW_DOWNLOADS=ON \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
         -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
-        -DENABLE_JPEG=OFF \
-        -DENABLE_JPEG_LOSSLESS=OFF \
+        -DDCMTK_LIBRARIES=dcmjpls \
 	~/Orthanc
 
 Note: Have also a look at the official package:
 http://anonscm.debian.org/viewvc/debian-med/trunk/packages/orthanc/trunk/debian/
 
 
-SUPPORTED - Ubuntu 12.04 LTS
-----------------------------
+SUPPORTED - Ubuntu 12.04.5 LTS
+------------------------------
 
 # sudo apt-get install build-essential unzip cmake mercurial \
        	       	       uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \
        	       	       libgtest-dev libpng-dev libsqlite3-dev libssl-dev \
-		       zlib1g-dev libdcmtk2-dev libboost-all-dev libwrap0-dev
+		       zlib1g-dev libdcmtk2-dev libboost1.48-all-dev libwrap0-dev \
+                       libcharls-dev
 
-# cmake "-DDCMTK_LIBRARIES=wrap;oflog" \
+# cmake "-DDCMTK_LIBRARIES=boost_locale;CharLS;dcmjpls;wrap;oflog" \
         -DALLOW_DOWNLOADS=ON \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
 	-DUSE_SYSTEM_JSONCPP=OFF \
 	-DUSE_SYSTEM_GOOGLE_LOG=OFF \
+	-DUSE_SYSTEM_PUGIXML=OFF \
         -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
-        -DENABLE_JPEG=OFF \
-        -DENABLE_JPEG_LOSSLESS=OFF \
 	~/Orthanc
 
 
@@ -155,6 +166,7 @@
 	-DUSE_SYSTEM_MONGOOSE=OFF \
 	-DUSE_SYSTEM_JSONCPP=OFF \
         -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+	-DUSE_SYSTEM_PUGIXML=OFF \
 	~/Orthanc
 
 
@@ -165,6 +177,7 @@
 	-DUSE_SYSTEM_MONGOOSE=OFF \
 	-DUSE_SYSTEM_JSONCPP=OFF \
         -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+	-DUSE_SYSTEM_PUGIXML=OFF \
         -DENABLE_JPEG=OFF \
         -DENABLE_JPEG_LOSSLESS=OFF \
 	~/Orthanc
@@ -183,29 +196,52 @@
         -DALLOW_DOWNLOADS=ON \
 	-DUSE_SYSTEM_MONGOOSE=OFF \
         -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+	-DUSE_SYSTEM_PUGIXML=OFF \
         -DENABLE_JPEG=OFF \
         -DENABLE_JPEG_LOSSLESS=OFF \
 	~/Orthanc
 
 
 
-SUPPORTED - Fedora 18/19/20
----------------------------
+SUPPORTED - Fedora 19/20
+------------------------
 
 # sudo yum install make automake gcc gcc-c++ python cmake \
                    boost-devel curl-devel dcmtk-devel glog-devel \
                    gtest-devel libpng-devel libsqlite3x-devel libuuid-devel \
-                   mongoose-devel openssl-devel jsoncpp-devel lua-devel
+                   mongoose-devel openssl-devel jsoncpp-devel lua-devel pugixml-devel
 
-# cmake -DENABLE_JPEG=OFF \
-        -DENABLE_JPEG_LOSSLESS=OFF \
-        ~/Orthanc
+# cmake  "-DDCMTK_LIBRARIES=CharLS" \
+         -DSYSTEM_MONGOOSE_USE_CALLBACKS=OFF \
+         ~/Orthanc
        
 Note: Have also a look at the official package:
 http://pkgs.fedoraproject.org/cgit/orthanc.git/tree/?h=f18
 
 
 
+SUPPORTED - Ubuntu 14.04 LTS
+----------------------------
+
+# cmake -DALLOW_DOWNLOADS=ON \
+        -DUSE_GTEST_DEBIAN_SOURCE_PACKAGE=ON \
+        -DUSE_SYSTEM_MONGOOSE=OFF \
+        -DDCMTK_LIBRARIES=dcmjpls \
+        ~/Orthanc
+
+
+
+SUPPORTED - FreeBSD 10.1
+------------------------
+
+# pkg install jsoncpp pugixml lua51 curl googletest dcmtk cmake \
+              e2fsprogs-libuuid glog boost-libs sqlite3 python libiconv
+
+# cmake -DALLOW_DOWNLOADS=ON \
+        -DUSE_SYSTEM_MONGOOSE=OFF \
+        -DDCMTK_LIBRARIES="dcmdsig;charls;dcmjpls" \
+	~/Orthanc
+
 
 
 Other Linux distributions?
--- a/NEWS	Wed Jun 25 15:34:40 2014 +0200
+++ b/NEWS	Thu May 21 16:58:30 2015 +0200
@@ -1,9 +1,193 @@
 Pending changes in the mainline
 ===============================
 
-* Official support of OS X (Darwin)
+Major
+-----
+
+* "?expand" flag for URIs "/patients", "/studies" and "/series"
+* "/tools/find" URI to search for DICOM resources from REST
+* Support of FreeBSD
+
+Minor
+-----
+
+* Speed-up in Orthanc Explorer for large amount of images
+* Speed-up of the C-Find SCP server of Orthanc
+* Allow replacing PatientID/StudyInstanceUID/SeriesInstanceUID from Lua scripts
+
+Fixes
+-----
+
+* Prevent freeze on C-FIND if no DICOM tag is to be returned
+* Fix slow C-Store SCP on recent versions of Linux, if
+  USE_SYSTEM_DCMTK is set to OFF (http://forum.dcmtk.org/viewtopic.php?f=1&t=4009)
+* Fix issue 30 (QR response missing "Query/Retrieve Level" (008,0052))
+* Fix issue 32 (Cyrillic symbols): Introduction of the "Windows1251" encoding
+* Plugins now receive duplicated GET arguments in their REST callbacks
+
+
+Version 0.8.6 (2015/02/12)
+==========================
+
+Major
+-----
+
+* URIs to get all the parents of a given resource in a single REST call
+* Instances without PatientID are now allowed
+* Support of HTTP proxy to access Orthanc peers
+
+Minor
+-----
+
+* Support of Tudor DICOM in Query/Retrieve
+* More flexible "/modify" and "/anonymize" for single instance
+* Access to called AET and remote AET from Lua scripts ("OnStoredInstance")
+* Option "DicomAssociationCloseDelay" to set delay before closing DICOM association
+* ZIP archives now display the accession number of the studies
+
+Plugins
+-------
+
+* Introspection of plugins (cf. the "/plugins" URI)
+* Plugins can access the command-line arguments used to launch Orthanc
+* Plugins can extend Orthanc Explorer with custom JavaScript
+* Plugins can get/set global properties to save their configuration
+* Plugins can do REST calls to other plugins (cf. "xxxAfterPlugins()")
+* Scan of folders for plugins
+
+Fixes
+-----
+
+* Code refactorings
+* Fix issue 25 (AET with underscore not allowed)
+* Fix replacement and insertion of private DICOM tags
+* Fix anonymization generating non-portable DICOM files
+
+
+Version 0.8.5 (2014/11/04)
+==========================
+
+General
+-------
+
+* Major speed-up thanks to a new database schema
+* Plugins can monitor changes through callbacks
+* Download ZIP + DICOMDIR from Orthanc Explorer
+* Sample plugin framework to serve static resources (./Plugins/Samples/WebSkeleton/)
+
+Fixes
+-----
+
+* Fix issue 19 (YBR_FULL are decoded incorrectly)
+* Fix issue 21 (Microsoft Visual Studio precompiled headers)
+* Fix issue 22 (Error decoding multi-frame instances)
+* Fix issue 24 (Build fails on OSX when directory has .DS_Store files)
+* Fix crash when bad HTTP credentials are provided
+
+
+Version 0.8.4 (2014/09/19)
+==========================
+
+* "/instances-tags" to get the tags of all the child instances of a
+  patient/study/series with a single REST call (bulk tags retrieval)
+* Configuration/Lua to select the accepted C-Store SCP transfer syntaxes
+* Fix reporting of errors in Orthanc Explorer when sending images to peers/modalities
+* Installation of plugin SDK in CMake
+
+
+Version 0.8.3 (2014/09/11)
+==========================
+
+Major
+-----
+
+* Creation of ZIP archives for media storage, with DICOMDIR
+* URIs to get all the children of a given resource in a single REST call
+* "/tools/lookup" URI to map DICOM UIDs to Orthanc identifiers
+* Support of index-only mode (using the "StoreDicom" option)
+* Plugins can implement a custom storage area
+
+Minor
+-----
+
+* Configuration option to enable HTTP Keep-Alive
+* Configuration option to disable the logging of exported resources in "/exports"
+* Plugins can retrieve the path to Orthanc and to its configuration file
+* "/tools/create-dicom" now accepts the "PatientID" DICOM tag (+ updated sample)
+* Possibility to set HTTP headers from plugins
+* "LastUpdate" metadata is now always returned for patients, studies and series
+
+Maintenance
+-----------
+
+* Refactoring of HttpOutput ("Content-Length" header is now always sent)
+* Upgrade to Mongoose 3.8
+* Fixes for Visual Studio 2013 and Windows 64bit
+* Fix issue 16: Handling of "AT" value representations in JSON
+* Fix issue 17
+
+
+Version 0.8.2 (2014/08/07)
+==========================
+
+* Support of the standard text encodings
+* Hot restart of Orthanc by posting to "/tools/reset"
+* More fault-tolerant commands in Lua scripts
+* Parameter to set the default encoding for DICOM files without SpecificCharacterSet
+* Fix of issue #14 (support of XCode 5.1)
+* Upgrade to Google Test 1.7.0
+
+
+Version 0.8.1 (2014/07/29)
+==========================
+
+General
+-------
+
+* Access patient module at the study level to cope with PatientID collisions
+* On-the-fly conversion of JSON to XML according to the HTTP Accept header
+* C-Echo SCU in the REST API
+* DICOM conformance statement available at URI "/tools/dicom-conformance"
+
+Lua scripts
+-----------
+
+* Lua scripts can do HTTP requests, and thus can call Web services
+* Lua scripts can invoke system commands, with CallSystem()
+
+Plugins
+-------
+
+* Lookup for DICOM UIDs in the plugin SDK
+* Plugins have access to the HTTP headers and can answer with HTTP status codes
+* Callback to react to the incoming of DICOM instances
+
+Fixes
+-----
+
+* Fix build of Google Log with Visual Studio >= 11.0
+* Fix automated generation of the list of resource children in the REST API
+
+
+Version 0.8.0 (2014/07/10)
+==========================
+
+Major changes
+-------------
+
+* Routing images with Lua scripts
+* Introduction of the Orthanc Plugin SDK
+* Official support of OS X (Darwin) 10.8
+
+Minor changes
+-------------
+
+* Extraction of tags for the patient/study/series/instance DICOM modules
+* Extraction of the tags shared by all the instances of a patient/study/series
 * Options to limit the number of results for an incoming C-FIND query
 * Support of kFreeBSD
+* Several code refactorings
+* Fix OrthancCppClient::GetVoxelSizeZ()
 
 
 Version 0.7.6 (2014/06/11)
@@ -21,7 +205,7 @@
 * Creation of DICOM instances using the REST API
 * Embedding of images within DICOM instances
 * Adding/removal/modification of remote modalities/peers through REST
-* Reuse of the previous SCU connection to avoid unecessary handshakes
+* Reuse of the previous SCU connection to avoid unnecessary handshakes
 * Fix problems with anonymization and modification
 * Fix missing licensing terms about reuse of some code from DCMTK
 * Various code refactorings
@@ -150,7 +334,7 @@
 ==========================
 
 * "Bulk" Store-SCU (send several DICOM instances with the same
-  DICOM connexion)
+  DICOM connection)
 * Store-SCU for patients and studies in Orthanc Explorer
 * Filtering of incoming DICOM instances (through Lua scripting)
 * Filtering of incoming HTTP requests (through Lua scripting)
@@ -175,7 +359,7 @@
 -------------
 
 * Download of modified or anonymized DICOM instances
-* Inplace modification and anymization of DICOM series, studies and patients
+* Inplace modification and anonymization of DICOM series, studies and patients
 
 Minor changes
 -------------
--- a/OrthancCppClient/Instance.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/Instance.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancCppClient/Instance.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/Instance.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancCppClient/OrthancClientException.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/OrthancClientException.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancCppClient/OrthancConnection.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/OrthancConnection.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancCppClient/OrthancConnection.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/OrthancConnection.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancCppClient/OrthancCppClient.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/OrthancCppClient.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancCppClient/Patient.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/Patient.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancCppClient/Patient.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/Patient.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancCppClient/Series.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/Series.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -53,11 +53,12 @@
         /**
          * Compute the slice normal from Image Orientation Patient.
          * http://nipy.sourceforge.net/nibabel/dicom/dicom_orientation.html#dicom-z-from-slice
+         * http://dicomiseasy.blogspot.be/2013/06/getting-oriented-using-image-plane.html
          * http://www.itk.org/pipermail/insight-users/2003-September/004762.html
          **/
 
         std::vector<float> cosines;
-        someSlice.SplitVectorOfFloats(cosines, "ImageOrientationPatient");
+        someSlice.SplitVectorOfFloats(cosines, "ImageOrientationPatient");  // 0020-0037
 
         if (cosines.size() != 6)
         {
@@ -76,7 +77,7 @@
       float ComputeSliceLocation(Instance& instance) const
       {
         std::vector<float> ipp;
-        instance.SplitVectorOfFloats(ipp, "ImagePositionPatient");
+        instance.SplitVectorOfFloats(ipp, "ImagePositionPatient");  // 0020-0032
         if (ipp.size() != 3)
         {
           throw OrthancClientException(Orthanc::ErrorCode_BadFileFormat);
@@ -129,7 +130,7 @@
 
           if (instance_.GetPixelFormat() == format_)
           {
-            memcpy(p, instance_.GetBuffer(y), 2 * instance_.GetWidth());
+            memcpy(p, instance_.GetBuffer(y), GetBytesPerPixel(instance_.GetPixelFormat()) * instance_.GetWidth());
           }
           else if (instance_.GetPixelFormat() == PixelFormat_Grayscale8 &&
                    format_ == PixelFormat_RGB24)
@@ -212,32 +213,77 @@
     {
       if (GetInstanceCount() == 0)
       {
+        // Empty image, use some default value (should never happen)
+        voxelSizeX_ = 1;
+        voxelSizeY_ = 1;
+        voxelSizeZ_ = 1;
+        sliceThickness_ = 1;
+
         return true;
       }
 
-      Instance& i1 = GetInstance(0);
+      // Choose a reference slice
+      Instance& reference = GetInstance(0);
 
+      // Check that all the child instances share the same 3D parameters
       for (unsigned int i = 0; i < GetInstanceCount(); i++)
       {
         Instance& i2 = GetInstance(i);
 
-        if (std::string(i1.GetTagAsString("Columns")) != std::string(i2.GetTagAsString("Columns")) ||
-            std::string(i1.GetTagAsString("Rows")) != std::string(i2.GetTagAsString("Rows")) ||
-            std::string(i1.GetTagAsString("ImageOrientationPatient")) != std::string(i2.GetTagAsString("ImageOrientationPatient")) ||
-            std::string(i1.GetTagAsString("SliceThickness")) != std::string(i2.GetTagAsString("SliceThickness")) ||
-            std::string(i1.GetTagAsString("PixelSpacing")) != std::string(i2.GetTagAsString("PixelSpacing")))
+        if (std::string(reference.GetTagAsString("Columns")) != std::string(i2.GetTagAsString("Columns")) ||
+            std::string(reference.GetTagAsString("Rows")) != std::string(i2.GetTagAsString("Rows")) ||
+            std::string(reference.GetTagAsString("ImageOrientationPatient")) != std::string(i2.GetTagAsString("ImageOrientationPatient")) ||
+            std::string(reference.GetTagAsString("SliceThickness")) != std::string(i2.GetTagAsString("SliceThickness")) ||
+            std::string(reference.GetTagAsString("PixelSpacing")) != std::string(i2.GetTagAsString("PixelSpacing")))
         {
           return false;
         }              
       }
 
-      SliceLocator locator(GetInstance(0));
+
+      // Extract X/Y voxel size and slice thickness
+      std::string s = GetInstance(0).GetTagAsString("PixelSpacing");  // 0028-0030
+      size_t pos = s.find('\\');
+      assert(pos != std::string::npos);
+      std::string sy = s.substr(0, pos);
+      std::string sx = s.substr(pos + 1);
+
+      try
+      {
+        voxelSizeX_ = boost::lexical_cast<float>(sx);
+        voxelSizeY_ = boost::lexical_cast<float>(sy);
+      }
+      catch (boost::bad_lexical_cast)
+      {
+        throw OrthancClientException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      sliceThickness_ = GetInstance(0).GetTagAsFloat("SliceThickness");  // 0018-0050
+
+
+      // Compute the location of each slice to extract the voxel size along Z
+      voxelSizeZ_ = std::numeric_limits<float>::infinity();
+
+      SliceLocator locator(reference);
+      float referenceSliceLocation = locator.ComputeSliceLocation(reference);
+
       std::set<float> l;
       for (unsigned int i = 0; i < GetInstanceCount(); i++)
       {
-        l.insert(locator.ComputeSliceLocation(GetInstance(i)));
+        float location = locator.ComputeSliceLocation(GetInstance(i));
+        float distanceToReferenceSlice = fabs(location - referenceSliceLocation);
+
+        l.insert(location);
+
+        if (distanceToReferenceSlice > std::numeric_limits<float>::epsilon() &&
+            distanceToReferenceSlice < voxelSizeZ_)
+        {
+          voxelSizeZ_ = distanceToReferenceSlice;
+        }
       }
 
+
+      // Make sure that 2 slices do not share the same Z location
       return l.size() == GetInstanceCount();
     }
     catch (OrthancClientException)
@@ -275,10 +321,10 @@
     status_ = Status3DImage_NotTested;
     url_ = std::string(connection_.GetOrthancUrl()) + "/series/" + id_;
 
-    isVoxelSizeRead_ = false;
     voxelSizeX_ = 0;
     voxelSizeY_ = 0;
     voxelSizeZ_ = 0;
+    sliceThickness_ = 0;
 
     instances_.SetThreadCount(connection.GetThreadCount());
   }
@@ -324,46 +370,6 @@
       return GetInstance(0).GetTagAsInt("Rows");
   }
 
-  void Series::LoadVoxelSize()
-  {
-    if (isVoxelSizeRead_)
-    {
-      return;
-    }
-
-    Check3DImage();
-
-    if (GetInstanceCount() == 0)
-    {
-      // Empty image, use some default value
-      voxelSizeX_ = 1;
-      voxelSizeY_ = 1;
-      voxelSizeZ_ = 1;
-    }
-    else
-    {
-      try
-      {
-        std::string s = GetInstance(0).GetTagAsString("PixelSpacing");
-        size_t pos = s.find('\\');
-        assert(pos != std::string::npos);
-        std::string sy = s.substr(0, pos);
-        std::string sx = s.substr(pos + 1);
-
-        voxelSizeX_ = boost::lexical_cast<float>(sx);
-        voxelSizeY_ = boost::lexical_cast<float>(sy);
-        voxelSizeZ_ = GetInstance(0).GetTagAsFloat("SliceThickness");
-      }
-      catch (boost::bad_lexical_cast)
-      {
-        throw OrthancClientException(Orthanc::ErrorCode_NotImplemented);
-      }
-    }
-
-    isVoxelSizeRead_ = true;
-  }
-
-
   const char* Series::GetMainDicomTag(const char* tag, const char* defaultValue) const
   {
     if (series_["MainDicomTags"].isMember(tag))
@@ -486,22 +492,28 @@
 
   float Series::GetVoxelSizeX()
   {
-    LoadVoxelSize();
+    Check3DImage();   // Is3DImageInternal() will compute the voxel sizes
     return voxelSizeX_;
   }
 
   float Series::GetVoxelSizeY()
   {
-    LoadVoxelSize();
+    Check3DImage();   // Is3DImageInternal() will compute the voxel sizes
     return voxelSizeY_;
   }
 
   float Series::GetVoxelSizeZ()
   {
-    LoadVoxelSize();
+    Check3DImage();   // Is3DImageInternal() will compute the voxel sizes
     return voxelSizeZ_;
   }
 
+  float Series::GetSliceThickness()
+  {
+    Check3DImage();   // Is3DImageInternal() will compute the voxel sizes
+    return sliceThickness_;
+  }
+
   void Series::Load3DImage(void* target,
                            Orthanc::PixelFormat format,
                            int64_t lineStride,
--- a/OrthancCppClient/Series.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/Series.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -62,11 +62,11 @@
     Orthanc::ArrayFilledByThreads  instances_;
     Status3DImage status_;
 
-    bool isVoxelSizeRead_;
     float voxelSizeX_;
     float voxelSizeY_;
     float voxelSizeZ_;
-  
+    float sliceThickness_;
+
     void Check3DImage();
 
     bool Is3DImageInternal();
@@ -86,8 +86,6 @@
                              size_t stackStride,
                              Orthanc::ThreadedCommandProcessor::IListener* listener);
 
-    void LoadVoxelSize();  
-
   public:
     /**
      * {summary}{Create a connection to some series.}
@@ -189,6 +187,13 @@
      **/
     float GetVoxelSizeZ();
 
+    /**
+     * {summary}{Get the slice thickness.}
+     * {description}{Get the slice thickness. This call is only valid if this series corresponds to a 3D image.}
+     * {returns}{The slice thickness.}
+     **/
+    float GetSliceThickness();
+
     LAAW_API_INTERNAL void Load3DImage(void* target,
                                        Orthanc::PixelFormat format,
                                        int64_t lineStride,
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/ExternC.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/ExternC.cpp	Thu May 21 16:58:30 2015 +0200
@@ -795,6 +795,29 @@
         }
       }
 
+      LAAW_EXPORT_DLL_API char* LAAW_CALL_CONVENTION LAAW_EXTERNC_2be452e7af5bf7dfd8c5021842674497(void* thisObject, float* result)
+      {
+        try
+        {
+          #ifdef LAAW_EXTERNC_START_FUNCTION
+          LAAW_EXTERNC_START_FUNCTION;
+          #endif
+
+          OrthancClient::Series* this_ = static_cast<OrthancClient::Series*>(thisObject);
+*result = this_->GetSliceThickness();
+
+          return NULL;
+        }
+        catch (::Laaw::LaawException& e)
+        {
+          return LAAW_EXTERNC_CopyString(e.What());
+        }
+        catch (...)
+        {
+          return LAAW_EXTERNC_CopyString("...");
+        }
+      }
+
       LAAW_EXPORT_DLL_API char* LAAW_CALL_CONVENTION LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5(void* thisObject, void* arg0, int32_t arg1, int64_t arg2, int64_t arg3)
       {
         try
@@ -1466,7 +1489,7 @@
 
   LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetCompany()
   {
-    return "CHU of Liege";
+    return "University Hospital of Liege";
   }
 
   LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetProduct()
@@ -1476,22 +1499,22 @@
 
   LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetCopyright()
   {
-    return "(c) 2012-2014, Sebastien Jodogne, CHU of Liege";
+    return "(c) 2012-2015, Sebastien Jodogne, University Hospital of Liege";
   }
 
   LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetVersion()
   {
-    return "0.7";
+    return "0.8";
   }
 
   LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetFileVersion()
   {
-    return "0.7.0.6";
+    return "0.8.0.6";
   }
 
   LAAW_EXPORT_DLL_API const char* LAAW_CALL_CONVENTION LAAW_EXTERNC_GetFullVersion()
   {
-    return "0.7.6";
+    return "0.8.6";
   }
 
   LAAW_EXPORT_DLL_API void LAAW_CALL_CONVENTION LAAW_EXTERNC_FreeString(char* str)
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/OrthancCppClient.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/OrthancCppClient.h	Thu May 21 16:58:30 2015 +0200
@@ -81,9 +81,9 @@
 
 /* cf. http://sourceforge.net/p/predef/wiki/Architectures/ */
 #ifdef __amd64__
-#define LAAW_ORTHANC_CLIENT_DEFAULT_PATH  "libOrthancClient.so.0.7"
+#define LAAW_ORTHANC_CLIENT_DEFAULT_PATH  "libOrthancClient.so.0.8"
 #else
-#define LAAW_ORTHANC_CLIENT_DEFAULT_PATH  "libOrthancClient.so.0.7"
+#define LAAW_ORTHANC_CLIENT_DEFAULT_PATH  "libOrthancClient.so.0.8"
 #endif
 
 #define LAAW_ORTHANC_CLIENT_CALL_CONV
@@ -203,7 +203,7 @@
   {
   private:
     LAAW_ORTHANC_CLIENT_HANDLE_TYPE  handle_;
-    LAAW_ORTHANC_CLIENT_FUNCTION_TYPE  functionsIndex_[62 + 1];
+    LAAW_ORTHANC_CLIENT_FUNCTION_TYPE  functionsIndex_[63 + 1];
 
 
 
@@ -239,7 +239,7 @@
     void FreeString(char* str)
     {
       typedef void (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (char*);
-      Function function = (Function) GetFunction(62);
+      Function function = (Function) GetFunction(63);
       function(str);
     }
 
@@ -302,7 +302,15 @@
     {
       if (handle_ != LAAW_ORTHANC_CLIENT_HANDLE_NULL)
       {
+#if 0
+        /**
+         * Do not explicitly unload the shared library, as it might
+         * interfere with the destruction of static objects declared
+         * inside the library (e.g. this is the case of gflags that is
+         * internally used by googlelog).
+         **/
         LAAW_ORTHANC_CLIENT_CLOSER(handle_);
+#endif
         handle_ = LAAW_ORTHANC_CLIENT_HANDLE_NULL;
       }
     }
@@ -386,12 +394,12 @@
    * It is assumed that the API does not change when the revision
    * number (MAJOR.MINOR.REVISION) changes.
    **/
-  if (strcmp(getVersion(), "0.7"))
+  if (strcmp(getVersion(), "0.8"))
   {
     throw ::OrthancClient::OrthancClientException("Mismatch between the C++ header and the library version");
   }
 
-  functionsIndex_[62] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_FreeString", "4");
+  functionsIndex_[63] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_FreeString", "4");
   functionsIndex_[3] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_557aee7b61817292a0f31269d3c35db7", "8");
   functionsIndex_[4] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_0b8dff0ce67f10954a49b059e348837e", "8");
   functionsIndex_[5] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_e05097c153f676e5a5ee54dcfc78256f", "4");
@@ -423,40 +431,41 @@
   functionsIndex_[30] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_b794f5cd3dad7d7b575dd1fd902afdd0", "8");
   functionsIndex_[31] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_8ee2e50dd9df8f66a3c1766090dd03ab", "8");
   functionsIndex_[32] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_046aed35bbe4751691f4c34cc249a61d", "8");
-  functionsIndex_[33] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5", "28");
-  functionsIndex_[34] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_b2601a161c24ad0a1d3586246f87452c", "32");
+  functionsIndex_[33] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_2be452e7af5bf7dfd8c5021842674497", "8");
+  functionsIndex_[34] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5", "28");
+  functionsIndex_[35] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_b2601a161c24ad0a1d3586246f87452c", "32");
   functionsIndex_[19] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_193599b9e345384fcdfcd47c29c55342", "12");
   functionsIndex_[20] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_7c97f17063a357d38c5fab1136ad12a0", "4");
-  functionsIndex_[37] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_e65b20b7e0170b67544cd6664a4639b7", "4");
-  functionsIndex_[38] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_470e981b0e41f17231ba0ae6f3033321", "8");
-  functionsIndex_[39] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_04cefd138b6ea15ad909858f2a0a8f05", "12");
-  functionsIndex_[40] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_aee5b1f6f0c082f2c3b0986f9f6a18c7", "8");
-  functionsIndex_[41] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_93965682bace75491413e1f0b8d5a654", "16");
-  functionsIndex_[35] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_b01c6003238eb46c8db5dc823d7ca678", "12");
-  functionsIndex_[36] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_0147007fb99bad8cd95a139ec8795376", "4");
-  functionsIndex_[44] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_236ee8b403bc99535a8a4695c0cd45cb", "8");
-  functionsIndex_[45] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_2a437b7aba6bb01e81113835be8f0146", "8");
-  functionsIndex_[46] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_2bcbcb850934ae0bb4c6f0cc940e6cda", "8");
-  functionsIndex_[47] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_8d415c3a78a48e7e61d9fd24e7c79484", "12");
-  functionsIndex_[48] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_70d2f8398bbc63b5f792b69b4ad5fecb", "12");
-  functionsIndex_[49] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1729a067d902771517388eedd7346b23", "12");
-  functionsIndex_[50] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_72e2aeee66cd3abd8ab7e987321c3745", "8");
-  functionsIndex_[51] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1ea3df5a1ac1a1a687fe7325adddb6f0", "8");
-  functionsIndex_[52] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_99b4f370e4f532d8b763e2cb49db92f8", "8");
-  functionsIndex_[53] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_c41c742b68617f1c0590577a0a5ebc0c", "8");
-  functionsIndex_[54] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_142dd2feba0fc1d262bbd0baeb441a8b", "8");
-  functionsIndex_[55] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_5f5c9f81a4dff8daa6c359f1d0488fef", "12");
-  functionsIndex_[56] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_9ca979fffd08fa256306d4e68d8b0e91", "8");
-  functionsIndex_[57] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_6f2d77a26edc91c28d89408dbc3c271e", "8");
-  functionsIndex_[58] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_c0f494b80d4ff8b232df7a75baa0700a", "4");
-  functionsIndex_[59] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_d604f44bd5195e082e745e9cbc164f4c", "4");
-  functionsIndex_[60] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1710299d1c5f3b1f2b7cf3962deebbfd", "8");
-  functionsIndex_[61] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_bb55aaf772ddceaadee36f4e54136bcb", "8");
-  functionsIndex_[42] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_6c5ad02f91b583e29cebd0bd319ce21d", "12");
-  functionsIndex_[43] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_4068241c44a9c1367fe0e57be523f207", "4");
+  functionsIndex_[38] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_e65b20b7e0170b67544cd6664a4639b7", "4");
+  functionsIndex_[39] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_470e981b0e41f17231ba0ae6f3033321", "8");
+  functionsIndex_[40] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_04cefd138b6ea15ad909858f2a0a8f05", "12");
+  functionsIndex_[41] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_aee5b1f6f0c082f2c3b0986f9f6a18c7", "8");
+  functionsIndex_[42] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_93965682bace75491413e1f0b8d5a654", "16");
+  functionsIndex_[36] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_b01c6003238eb46c8db5dc823d7ca678", "12");
+  functionsIndex_[37] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_0147007fb99bad8cd95a139ec8795376", "4");
+  functionsIndex_[45] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_236ee8b403bc99535a8a4695c0cd45cb", "8");
+  functionsIndex_[46] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_2a437b7aba6bb01e81113835be8f0146", "8");
+  functionsIndex_[47] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_2bcbcb850934ae0bb4c6f0cc940e6cda", "8");
+  functionsIndex_[48] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_8d415c3a78a48e7e61d9fd24e7c79484", "12");
+  functionsIndex_[49] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_70d2f8398bbc63b5f792b69b4ad5fecb", "12");
+  functionsIndex_[50] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1729a067d902771517388eedd7346b23", "12");
+  functionsIndex_[51] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_72e2aeee66cd3abd8ab7e987321c3745", "8");
+  functionsIndex_[52] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1ea3df5a1ac1a1a687fe7325adddb6f0", "8");
+  functionsIndex_[53] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_99b4f370e4f532d8b763e2cb49db92f8", "8");
+  functionsIndex_[54] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_c41c742b68617f1c0590577a0a5ebc0c", "8");
+  functionsIndex_[55] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_142dd2feba0fc1d262bbd0baeb441a8b", "8");
+  functionsIndex_[56] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_5f5c9f81a4dff8daa6c359f1d0488fef", "12");
+  functionsIndex_[57] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_9ca979fffd08fa256306d4e68d8b0e91", "8");
+  functionsIndex_[58] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_6f2d77a26edc91c28d89408dbc3c271e", "8");
+  functionsIndex_[59] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_c0f494b80d4ff8b232df7a75baa0700a", "4");
+  functionsIndex_[60] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_d604f44bd5195e082e745e9cbc164f4c", "4");
+  functionsIndex_[61] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_1710299d1c5f3b1f2b7cf3962deebbfd", "8");
+  functionsIndex_[62] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_bb55aaf772ddceaadee36f4e54136bcb", "8");
+  functionsIndex_[43] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_6c5ad02f91b583e29cebd0bd319ce21d", "12");
+  functionsIndex_[44] = LAAW_ORTHANC_CLIENT_GET_FUNCTION(handle_, "LAAW_EXTERNC_4068241c44a9c1367fe0e57be523f207", "4");
   
   /* Check whether the functions were properly loaded */
-  for (unsigned int i = 0; i <= 62; i++)
+  for (unsigned int i = 0; i <= 63; i++)
   {
     if (functionsIndex_[i] == (LAAW_ORTHANC_CLIENT_FUNCTION_TYPE) NULL)
     {
@@ -704,6 +713,7 @@
     inline float GetVoxelSizeX();
     inline float GetVoxelSizeY();
     inline float GetVoxelSizeZ();
+    inline float GetSliceThickness();
     inline void Load3DImage(void* target, ::Orthanc::PixelFormat format, LAAW_INT64 lineStride, LAAW_INT64 stackStride);
     inline void Load3DImage(void* target, ::Orthanc::PixelFormat format, LAAW_INT64 lineStride, LAAW_INT64 stackStride, float progress[]);
   };
@@ -1323,6 +1333,22 @@
     return result_;
   }
   /**
+  * @brief Get the slice thickness.
+  *
+  * Get the slice thickness. This call is only valid if this series corresponds to a 3D image.
+  *
+  * @return The slice thickness.
+  **/
+  inline float Series::GetSliceThickness()
+  {
+    float result_;
+    typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, float*);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(33);
+    char* error = function(pimpl_, &result_);
+    ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
+    return result_;
+  }
+  /**
   * @brief Load the 3D image into a memory buffer.
   *
   * Load the 3D image into a memory buffer. This call is only valid if this series corresponds to a 3D image. The "target" buffer must be wide enough to store all the voxels of the image.
@@ -1335,7 +1361,7 @@
   inline void Series::Load3DImage(void* target, ::Orthanc::PixelFormat format, LAAW_INT64 lineStride, LAAW_INT64 stackStride)
   {
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, void*, LAAW_INT32, LAAW_INT64, LAAW_INT64);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(33);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(34);
     char* error = function(pimpl_, target, format, lineStride, stackStride);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
   }
@@ -1353,7 +1379,7 @@
   inline void Series::Load3DImage(void* target, ::Orthanc::PixelFormat format, LAAW_INT64 lineStride, LAAW_INT64 stackStride, float progress[])
   {
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, void*, LAAW_INT32, LAAW_INT64, LAAW_INT64, float*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(34);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(35);
     char* error = function(pimpl_, target, format, lineStride, stackStride, progress);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
   }
@@ -1373,7 +1399,7 @@
   {
     isReference_ = false;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void**, void*, const char*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(35);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(36);
     char* error = function(&pimpl_, connection.pimpl_, id.c_str());
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
   }
@@ -1387,7 +1413,7 @@
   {
     if (isReference_) return;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(36);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(37);
     char* error = function(pimpl_);
     error = error;  // Remove warning about unused variable
   }
@@ -1400,7 +1426,7 @@
   inline void Study::Reload()
   {
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(37);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(38);
     char* error = function(pimpl_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
   }
@@ -1415,7 +1441,7 @@
   {
     LAAW_UINT32 result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_UINT32*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(38);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(39);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return result_;
@@ -1432,7 +1458,7 @@
   {
     void* result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, void**, LAAW_UINT32);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(39);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(40);
     char* error = function(pimpl_, &result_, index);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return ::OrthancClient::Series(result_);
@@ -1448,7 +1474,7 @@
   {
     const char* result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, const char**);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(40);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(41);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return std::string(result_);
@@ -1466,7 +1492,7 @@
   {
     const char* result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, const char**, const char*, const char*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(41);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(42);
     char* error = function(pimpl_, &result_, tag.c_str(), defaultValue.c_str());
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return std::string(result_);
@@ -1487,7 +1513,7 @@
   {
     isReference_ = false;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void**, void*, const char*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(42);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(43);
     char* error = function(&pimpl_, connection.pimpl_, id.c_str());
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
   }
@@ -1501,7 +1527,7 @@
   {
     if (isReference_) return;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(43);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(44);
     char* error = function(pimpl_);
     error = error;  // Remove warning about unused variable
   }
@@ -1516,7 +1542,7 @@
   {
     const char* result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, const char**);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(44);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(45);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return std::string(result_);
@@ -1531,7 +1557,7 @@
   inline void Instance::SetImageExtractionMode(::Orthanc::ImageExtractionMode mode)
   {
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_INT32);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(45);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(46);
     char* error = function(pimpl_, mode);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
   }
@@ -1546,7 +1572,7 @@
   {
     LAAW_INT32 result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, LAAW_INT32*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(46);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(47);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return static_cast< ::Orthanc::ImageExtractionMode >(result_);
@@ -1563,7 +1589,7 @@
   {
     const char* result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, const char**, const char*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(47);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(48);
     char* error = function(pimpl_, &result_, tag.c_str());
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return std::string(result_);
@@ -1580,7 +1606,7 @@
   {
     float result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, float*, const char*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(48);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(49);
     char* error = function(pimpl_, &result_, tag.c_str());
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return result_;
@@ -1597,7 +1623,7 @@
   {
     LAAW_INT32 result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, LAAW_INT32*, const char*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(49);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(50);
     char* error = function(pimpl_, &result_, tag.c_str());
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return result_;
@@ -1613,7 +1639,7 @@
   {
     LAAW_UINT32 result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_UINT32*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(50);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(51);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return result_;
@@ -1629,7 +1655,7 @@
   {
     LAAW_UINT32 result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_UINT32*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(51);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(52);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return result_;
@@ -1645,7 +1671,7 @@
   {
     LAAW_UINT32 result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_UINT32*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(52);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(53);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return result_;
@@ -1661,7 +1687,7 @@
   {
     LAAW_INT32 result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_INT32*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(53);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(54);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return static_cast< ::Orthanc::PixelFormat >(result_);
@@ -1677,7 +1703,7 @@
   {
     const void* result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, const void**);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(54);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(55);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return reinterpret_cast< const void* >(result_);
@@ -1694,7 +1720,7 @@
   {
     const void* result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, const void**, LAAW_UINT32);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(55);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(56);
     char* error = function(pimpl_, &result_, y);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return reinterpret_cast< const void* >(result_);
@@ -1710,7 +1736,7 @@
   {
     LAAW_UINT64 result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, LAAW_UINT64*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(56);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(57);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return result_;
@@ -1726,7 +1752,7 @@
   {
     const void* result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, const void**);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(57);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(58);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return reinterpret_cast< const void* >(result_);
@@ -1740,7 +1766,7 @@
   inline void Instance::DiscardImage()
   {
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(58);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(59);
     char* error = function(pimpl_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
   }
@@ -1753,7 +1779,7 @@
   inline void Instance::DiscardDicom()
   {
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(59);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(60);
     char* error = function(pimpl_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
   }
@@ -1767,7 +1793,7 @@
   inline void Instance::LoadTagContent(const ::std::string& path)
   {
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (void*, const char*);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(60);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(61);
     char* error = function(pimpl_, path.c_str());
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
   }
@@ -1782,7 +1808,7 @@
   {
     const char* result_;
     typedef char* (LAAW_ORTHANC_CLIENT_CALL_CONV* Function) (const void*, const char**);
-    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(61);
+    Function function = (Function) ::OrthancClient::Internals::Library::GetInstance().GetFunction(62);
     char* error = function(pimpl_, &result_);
     ::OrthancClient::Internals::Library::GetInstance().ThrowExceptionIfNeeded(error);
     return std::string(result_);
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows32.def	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows32.def	Thu May 21 16:58:30 2015 +0200
@@ -31,6 +31,7 @@
   _LAAW_EXTERNC_b794f5cd3dad7d7b575dd1fd902afdd0@8 = LAAW_EXTERNC_b794f5cd3dad7d7b575dd1fd902afdd0@8
   _LAAW_EXTERNC_8ee2e50dd9df8f66a3c1766090dd03ab@8 = LAAW_EXTERNC_8ee2e50dd9df8f66a3c1766090dd03ab@8
   _LAAW_EXTERNC_046aed35bbe4751691f4c34cc249a61d@8 = LAAW_EXTERNC_046aed35bbe4751691f4c34cc249a61d@8
+  _LAAW_EXTERNC_2be452e7af5bf7dfd8c5021842674497@8 = LAAW_EXTERNC_2be452e7af5bf7dfd8c5021842674497@8
   _LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5@28 = LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5@28
   _LAAW_EXTERNC_b2601a161c24ad0a1d3586246f87452c@32 = LAAW_EXTERNC_b2601a161c24ad0a1d3586246f87452c@32
   _LAAW_EXTERNC_193599b9e345384fcdfcd47c29c55342@12 = LAAW_EXTERNC_193599b9e345384fcdfcd47c29c55342@12
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows32.rc	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows32.rc	Thu May 21 16:58:30 2015 +0200
@@ -1,8 +1,8 @@
 #include <winver.h>
 
 VS_VERSION_INFO VERSIONINFO
-   FILEVERSION 0,7,0,6
-   PRODUCTVERSION 0,7,0,0
+   FILEVERSION 0,8,0,6
+   PRODUCTVERSION 0,8,0,0
    FILEOS VOS_NT_WINDOWS32
    FILETYPE VFT_DLL
    BEGIN
@@ -10,16 +10,16 @@
       BEGIN
          BLOCK "040904E4"
          BEGIN
-            VALUE "Comments", "Release 0.7.6"
-            VALUE "CompanyName", "CHU of Liege"
+            VALUE "Comments", "Release 0.8.6"
+            VALUE "CompanyName", "University Hospital of Liege"
             VALUE "FileDescription", "Native client to the REST API of Orthanc"
-            VALUE "FileVersion", "0.7.0.6"
+            VALUE "FileVersion", "0.8.0.6"
             VALUE "InternalName", "OrthancClient"
-            VALUE "LegalCopyright", "(c) 2012-2014, Sebastien Jodogne, CHU of Liege"
-            VALUE "LegalTrademarks", "Licensing information is available on https://code.google.com/p/orthanc/"
+            VALUE "LegalCopyright", "(c) 2012-2015, Sebastien Jodogne, University Hospital of Liege"
+            VALUE "LegalTrademarks", "Licensing information is available at http://www.orthanc-server.com/"
             VALUE "OriginalFilename", "OrthancClient_Windows32.dll"
             VALUE "ProductName", "OrthancClient"
-            VALUE "ProductVersion", "0.7"
+            VALUE "ProductVersion", "0.8"
          END
       END
 
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows64.def	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows64.def	Thu May 21 16:58:30 2015 +0200
@@ -31,6 +31,7 @@
   LAAW_EXTERNC_b794f5cd3dad7d7b575dd1fd902afdd0
   LAAW_EXTERNC_8ee2e50dd9df8f66a3c1766090dd03ab
   LAAW_EXTERNC_046aed35bbe4751691f4c34cc249a61d
+  LAAW_EXTERNC_2be452e7af5bf7dfd8c5021842674497
   LAAW_EXTERNC_4dcc7a0fd025efba251ac6e9b701c2c5
   LAAW_EXTERNC_b2601a161c24ad0a1d3586246f87452c
   LAAW_EXTERNC_193599b9e345384fcdfcd47c29c55342
--- a/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows64.rc	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/SharedLibrary/AUTOGENERATED/Windows64.rc	Thu May 21 16:58:30 2015 +0200
@@ -1,8 +1,8 @@
 #include <winver.h>
 
 VS_VERSION_INFO VERSIONINFO
-   FILEVERSION 0,7,0,6
-   PRODUCTVERSION 0,7,0,0
+   FILEVERSION 0,8,0,6
+   PRODUCTVERSION 0,8,0,0
    FILEOS VOS_NT_WINDOWS32
    FILETYPE VFT_DLL
    BEGIN
@@ -10,16 +10,16 @@
       BEGIN
          BLOCK "040904E4"
          BEGIN
-            VALUE "Comments", "Release 0.7.6"
-            VALUE "CompanyName", "CHU of Liege"
+            VALUE "Comments", "Release 0.8.6"
+            VALUE "CompanyName", "University Hospital of Liege"
             VALUE "FileDescription", "Native client to the REST API of Orthanc"
-            VALUE "FileVersion", "0.7.0.6"
+            VALUE "FileVersion", "0.8.0.6"
             VALUE "InternalName", "OrthancClient"
-            VALUE "LegalCopyright", "(c) 2012-2014, Sebastien Jodogne, CHU of Liege"
-            VALUE "LegalTrademarks", "Licensing information is available on https://code.google.com/p/orthanc/"
+            VALUE "LegalCopyright", "(c) 2012-2015, Sebastien Jodogne, University Hospital of Liege"
+            VALUE "LegalTrademarks", "Licensing information is available at http://www.orthanc-server.com/"
             VALUE "OriginalFilename", "OrthancClient_Windows64.dll"
             VALUE "ProductName", "OrthancClient"
-            VALUE "ProductVersion", "0.7"
+            VALUE "ProductVersion", "0.8"
          END
       END
 
--- a/OrthancCppClient/SharedLibrary/Product.json	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/SharedLibrary/Product.json	Thu May 21 16:58:30 2015 +0200
@@ -1,8 +1,8 @@
 {
   "Product" : "OrthancClient",
   "Description" : "Native client to the REST API of Orthanc",
-  "Company" : "CHU of Liege",
-  "Copyright" : "(c) 2012-2014, Sebastien Jodogne, CHU of Liege",
-  "Legal" : "Licensing information is available on https://code.google.com/p/orthanc/",
-  "Version" : "0.7.6"
+  "Company" : "University Hospital of Liege",
+  "Copyright" : "(c) 2012-2015, Sebastien Jodogne, University Hospital of Liege",
+  "Legal" : "Licensing information is available at http://www.orthanc-server.com/",
+  "Version" : "0.8.6"
 }
--- a/OrthancCppClient/SharedLibrary/SharedLibrary.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/SharedLibrary/SharedLibrary.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancCppClient/Study.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/Study.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancCppClient/Study.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancCppClient/Study.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancExplorer/explorer.html	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancExplorer/explorer.html	Thu May 21 16:58:30 2015 +0200
@@ -30,11 +30,13 @@
     <link rel="stylesheet" href="explorer.css" />
     <script src="file-upload.js"></script>
     <script src="explorer.js"></script>
+    <script src="../plugins/explorer.js"></script>
   </head>
   <body>
     <div data-role="page" id="find-patients" >
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Find a patient</h1>
+        <a href="#plugins" data-icon="grid" class="ui-btn-left" data-direction="reverse">Plugins</a>
         <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
       </div>
       <div data-role="content">
@@ -46,7 +48,7 @@
     <div data-role="page" id="upload" >
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Upload DICOM files</h1>
-        <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
       </div>
       <div data-role="content">
         <div style="display:none">
@@ -72,7 +74,7 @@
     <div data-role="page" id="patient" >
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Patient</h1>
-        <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
         <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
       </div>
       <div data-role="content">
@@ -105,6 +107,7 @@
                   <a href="#" id="patient-modified-from">Before modification</a>
                 </li>
                 <li data-icon="gear"><a href="#" id="patient-archive">Download ZIP</a></li>
+                <li data-icon="gear"><a href="#" id="patient-media">Download DICOMDIR</a></li>
               </ul>
             </div>
           </div>
@@ -125,7 +128,7 @@
           <a href="#" class="patient-link">Patient</a> &raquo; 
           Study
         </h1>
-        <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
         <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
       </div>
       <div data-role="content">
@@ -151,6 +154,7 @@
                   <a href="#" id="study-modified-from">Before modification</a>
                 </li>
                 <li data-icon="gear"><a href="#" id="study-archive">Download ZIP</a></li>
+                <li data-icon="gear"><a href="#" id="study-media">Download DICOMDIR</a></li>
               </ul>
             </div>
           </div>
@@ -173,7 +177,7 @@
           Series
         </h1>
 
-        <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
         <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
       </div>
       <div data-role="content">
@@ -200,6 +204,7 @@
                 </li>
                 <li data-icon="search"><a href="#" id="series-preview">Preview this series</a></li>
                 <li data-icon="gear"><a href="#" id="series-archive">Download ZIP</a></li>
+                <li data-icon="gear"><a href="#" id="series-media">Download DICOMDIR</a></li>
               </ul>
             </div>
           </div>
@@ -222,7 +227,7 @@
           <a href="#" class="series-link">Series</a> &raquo; 
           Instance
         </h1>
-        <a href="#find-patients" data-icon="search" class="ui-btn-left" data-direction="reverse">Find patient</a>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
         <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
       </div>
       <div data-role="content">
@@ -268,6 +273,18 @@
       </div>
     </div>
 
+    <div data-role="page" id="plugins" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>Plugins</h1>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+      </div>
+      <div data-role="content">
+        <ul id="all-plugins" data-role="listview" data-inset="true" data-filter="true">
+        </ul>
+      </div>
+    </div>
+
+
     <div id="peer-store" style="display:none;" class="ui-body-c">
       <p align="center"><b>Sending to Orthanc peer...</b></p>
       <p><img src="libs/images/ajax-loader2.gif" alt="" /></p>
--- a/OrthancExplorer/explorer.js	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancExplorer/explorer.js	Thu May 21 16:58:30 2015 +0200
@@ -142,11 +142,10 @@
 
 
 
-function GetSingleResource(type, uuid, callback)
+function GetResource(uri, callback)
 {
-  var resource = null;
   $.ajax({
-    url: '../' + type + '/' + uuid,
+    url: '..' + uri,
     dataType: 'json',
     async: false,
     cache: false,
@@ -157,42 +156,6 @@
 }
 
 
-function GetMultipleResources(type, uuids, callback)
-{
-  if (uuids == null)
-  {
-    $.ajax({
-      url: '../' + type,
-      dataType: 'json',
-      async: false,
-      cache: false,
-      success: function(s) {
-        uuids = s;
-      }
-    });
-  }
-
-  var resources = [];
-  var ajaxRequests = uuids.map(function(uuid) {
-    return $.ajax({
-      url: '../' + type + '/' + uuid,
-      dataType: 'json',
-      async: true,
-      cache: false,
-      success: function(s) {
-        resources.push(s);
-      }
-    });
-  });
-
-  // Wait for all the AJAX requests to end
-  $.when.apply($, ajaxRequests).then(function() {
-    callback(resources);
-  });
-}
-
-
-
 function CompleteFormatting(s, link, isReverse)
 {
   if (link != null)
@@ -344,18 +307,18 @@
 
 
 $('#find-patients').live('pagebeforeshow', function() {
-  GetMultipleResources('patients', null, function(patients) {
-    var target = $('#all-patients');
-    $('li', target).remove();
+  GetResource('/patients?expand', function(patients) {
+      var target = $('#all-patients');
+      $('li', target).remove();
     
-    SortOnDicomTag(patients, 'PatientName', false, false);
+      SortOnDicomTag(patients, 'PatientName', false, false);
 
-    for (var i = 0; i < patients.length; i++) {
-      var p = FormatPatient(patients[i], '#patient?uuid=' + patients[i].ID);
-      target.append(p);
-    }
+      for (var i = 0; i < patients.length; i++) {
+        var p = FormatPatient(patients[i], '#patient?uuid=' + patients[i].ID);
+        target.append(p);
+      }
 
-    target.listview('refresh');
+      target.listview('refresh');
   });
 });
 
@@ -381,8 +344,8 @@
 function RefreshPatient()
 {
   if ($.mobile.pageData) {
-    GetSingleResource('patients', $.mobile.pageData.uuid, function(patient) {
-      GetMultipleResources('studies', patient.Studies, function(studies) {
+    GetResource('/patients/' + $.mobile.pageData.uuid, function(patient) {
+      GetResource('/patients/' + $.mobile.pageData.uuid + '/studies', function(studies) {
         SortOnDicomTag(studies, 'StudyDate', false, true);
 
         $('#patient-info li').remove();
@@ -433,9 +396,9 @@
 function RefreshStudy()
 {
   if ($.mobile.pageData) {
-    GetSingleResource('studies', $.mobile.pageData.uuid, function(study) {
-      GetSingleResource('patients', study.ParentPatient, function(patient) {
-        GetMultipleResources('series', study.Series, function(series) {
+    GetResource('/studies/' + $.mobile.pageData.uuid, function(study) {
+      GetResource('/patients/' + study.ParentPatient, function(patient) {
+        GetResource('/studies/' + $.mobile.pageData.uuid + '/series', function(series) {
           SortOnDicomTag(series, 'SeriesDate', false, true);
 
           $('#study .patient-link').attr('href', '#patient?uuid=' + patient.ID);
@@ -465,7 +428,7 @@
           currentPage = 'study';
           currentUuid = $.mobile.pageData.uuid;
         });
-      });  
+      });
     });
   }
 }
@@ -474,10 +437,10 @@
 function RefreshSeries() 
 {
   if ($.mobile.pageData) {
-    GetSingleResource('series', $.mobile.pageData.uuid, function(series) {
-      GetSingleResource('studies', series.ParentStudy, function(study) {
-        GetSingleResource('patients', study.ParentPatient, function(patient) {
-          GetMultipleResources('instances', series.Instances, function(instances) {
+    GetResource('/series/' + $.mobile.pageData.uuid, function(series) {
+      GetResource('/studies/' + series.ParentStudy, function(study) {
+        GetResource('/patients/' + study.ParentPatient, function(patient) {
+          GetResource('/series/' + $.mobile.pageData.uuid + '/instances', function(instances) {
             Sort(instances, function(x) { return x.IndexInSeries; }, true, false);
 
             $('#series .patient-link').attr('href', '#patient?uuid=' + patient.ID);
@@ -568,10 +531,10 @@
 function RefreshInstance()
 {
   if ($.mobile.pageData) {
-    GetSingleResource('instances', $.mobile.pageData.uuid, function(instance) {
-      GetSingleResource('series', instance.ParentSeries, function(series) {
-        GetSingleResource('studies', series.ParentStudy, function(study) {
-          GetSingleResource('patients', study.ParentPatient, function(patient) {
+    GetResource('/instances/' + $.mobile.pageData.uuid, function(instance) {
+      GetResource('/series/' + instance.ParentSeries, function(series) {
+        GetResource('/studies/' + series.ParentStudy, function(study) {
+          GetResource('/patients/' + study.ParentPatient, function(patient) {
 
             $('#instance .patient-link').attr('href', '#patient?uuid=' + patient.ID);
             $('#instance .study-link').attr('href', '#study?uuid=' + study.ID);
@@ -589,13 +552,8 @@
               .append(FormatInstance(instance))
               .listview('refresh');
 
-            $.ajax({
-              url: '../instances/' + instance.ID + '/tags',
-              cache: false,
-              dataType: 'json',
-              success: function(s) {
-                $('#dicom-tree').tree('loadData', ConvertForTree(s));
-              }
+            GetResource('/instances/' + instance.ID + '/tags', function(s) {
+              $('#dicom-tree').tree('loadData', ConvertForTree(s));
             });
 
             SetupAnonymizedOrModifiedFrom('#instance-anonymized-from', instance, 'instance', 'AnonymizedFrom');
@@ -728,7 +686,7 @@
 
 $('#instance-preview').live('click', function(e) {
   if ($.mobile.pageData) {
-    GetSingleResource('instances', $.mobile.pageData.uuid + '/frames', function(frames) {
+    GetResource('/instances/' + $.mobile.pageData.uuid + '/frames', function(frames) {
       if (frames.length == 1)
       {
         // Viewing a single-frame image
@@ -759,10 +717,12 @@
   }
 });
 
+
+
 $('#series-preview').live('click', function(e) {
   if ($.mobile.pageData) {
-    GetSingleResource('series', $.mobile.pageData.uuid, function(series) {
-      GetMultipleResources('instances', series.Instances, function(instances) {
+    GetResource('/series/' + $.mobile.pageData.uuid, function(series) {
+      GetResource('/series/' + $.mobile.pageData.uuid + '/instances', function(instances) {
         Sort(instances, function(x) { return x.IndexInSeries; }, true, false);
 
         var images = [];
@@ -777,7 +737,7 @@
           imageFadeDuration : 1,
           loop : true
         });
-      })
+      });
     });
   }
 });
@@ -786,7 +746,6 @@
 
 
 
-
 function ChooseDicomModality(callback)
 {
   var clickedModality = '';
@@ -935,6 +894,24 @@
   window.location.href = '../series/' + $.mobile.pageData.uuid + '/archive';
 });
 
+
+$('#patient-media').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../patients/' + $.mobile.pageData.uuid + '/media';
+});
+
+$('#study-media').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../studies/' + $.mobile.pageData.uuid + '/media';
+});
+
+$('#series-media').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../series/' + $.mobile.pageData.uuid + '/media';
+});
+
+
+
 $('#protection').live('change', function(e) {
   var isProtected = e.target.value == "on";
   $.ajax({
@@ -1005,3 +982,46 @@
   OpenAnonymizeResourceDialog('../patients/' + $.mobile.pageData.uuid,
                               'Anonymize this patient?');
 });
+
+
+$('#plugins').live('pagebeforeshow', function() {
+  $.ajax({
+    url: '../plugins',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(plugins) {
+      var target = $('#all-plugins');
+      $('li', target).remove();
+
+      plugins.map(function(id) {
+        return $.ajax({
+          url: '../plugins/' + id,
+          dataType: 'json',
+          async: false,
+          cache: false,
+          success: function(plugin) {
+            var li = $('<li>');
+            var item = li;
+
+            if ('RootUri' in plugin)
+            {
+              item = $('<a>');
+              li.append(item);
+              item.click(function() {
+                window.open(plugin.RootUri);
+              });
+            }
+
+            item.append($('<h1>').text(plugin.ID));
+            item.append($('<p>').text(plugin.Description));
+            item.append($('<span>').addClass('ui-li-count').text(plugin.Version));
+            target.append(li);
+          }
+        });
+      });
+
+      target.listview('refresh');
+    }
+  });
+});
--- a/OrthancServer/DatabaseWrapper.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DatabaseWrapper.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -39,6 +39,7 @@
 
 #include <glog/logging.h>
 #include <stdio.h>
+#include <boost/lexical_cast.hpp>
 
 namespace Orthanc
 {
@@ -92,6 +93,35 @@
       }
     };
 
+    class SignalResourceDeleted : public SQLite::IScalarFunction
+    {
+    private:
+      IServerIndexListener& listener_;
+
+    public:
+      SignalResourceDeleted(IServerIndexListener& listener) :
+        listener_(listener)
+      {
+      }
+
+      virtual const char* GetName() const
+      {
+        return "SignalResourceDeleted";
+      }
+
+      virtual unsigned int GetCardinality() const
+      {
+        return 2;
+      }
+
+      virtual void Compute(SQLite::FunctionContext& context)
+      {
+        ResourceType type = static_cast<ResourceType>(context.GetIntValue(1));
+        ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0));
+        listener_.SignalChange(change);
+      }
+    };
+
     class SignalRemainingAncestor : public SQLite::IScalarFunction
     {
     private:
@@ -184,20 +214,6 @@
     }
   }
 
-  std::string DatabaseWrapper::GetGlobalProperty(GlobalProperty property,
-                                                 const std::string& defaultValue)
-  {
-    std::string s;
-    if (LookupGlobalProperty(s, property))
-    {
-      return s;
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
   int64_t DatabaseWrapper::CreateResource(const std::string& publicId,
                                           ResourceType type)
   {
@@ -205,38 +221,12 @@
     s.BindInt(0, type);
     s.BindString(1, publicId);
     s.Run();
-    int64_t id = db_.GetLastInsertRowId();
-
-    ChangeType changeType;
-    switch (type)
-    {
-    case ResourceType_Patient: 
-      changeType = ChangeType_NewPatient; 
-      break;
-
-    case ResourceType_Study: 
-      changeType = ChangeType_NewStudy; 
-      break;
-
-    case ResourceType_Series: 
-      changeType = ChangeType_NewSeries; 
-      break;
-
-    case ResourceType_Instance: 
-      changeType = ChangeType_NewInstance; 
-      break;
-
-    default:
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    LogChange(changeType, id, type);
-    return id;
+    return db_.GetLastInsertRowId();
   }
 
-  bool DatabaseWrapper::LookupResource(const std::string& publicId,
-                                       int64_t& id,
-                                       ResourceType& type)
+  bool DatabaseWrapper::LookupResource(int64_t& id,
+                                       ResourceType& type,
+                                       const std::string& publicId)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, 
                         "SELECT internalId, resourceType FROM Resources WHERE publicId=?");
@@ -320,16 +310,17 @@
     s.Run();
   }
 
-  void DatabaseWrapper::GetChildren(Json::Value& childrenPublicIds,
+
+  void DatabaseWrapper::GetChildren(std::list<std::string>& childrenPublicIds,
                                     int64_t id)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?");
     s.BindInt64(0, id);
 
-    childrenPublicIds = Json::arrayValue;
+    childrenPublicIds.clear();
     while (s.Step())
     {
-      childrenPublicIds.append(s.ColumnString(0));
+      childrenPublicIds.push_back(s.ColumnString(0));
     }
   }
 
@@ -342,10 +333,11 @@
     s.BindInt64(0, id);
     s.Run();
 
-    if (signalRemainingAncestor_->HasRemainingAncestor())
+    if (signalRemainingAncestor_->HasRemainingAncestor() &&
+        listener_ != NULL)
     {
-      listener_.SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(),
-                                        signalRemainingAncestor_->GetRemainingAncestorId());
+      listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(),
+                                         signalRemainingAncestor_->GetRemainingAncestorId());
     }
   }
 
@@ -404,45 +396,6 @@
   }
 
 
-  std::string DatabaseWrapper::GetMetadata(int64_t id,
-                                           MetadataType type,
-                                           const std::string& defaultValue)
-  {
-    std::string s;
-    if (LookupMetadata(s, id, type))
-    {
-      return s;
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-
-  bool DatabaseWrapper::GetMetadataAsInteger(int& result,
-                                             int64_t id,
-                                             MetadataType type)
-  {
-    std::string s = GetMetadata(id, type, "");
-    if (s.size() == 0)
-    {
-      return false;
-    }
-
-    try
-    {
-      result = boost::lexical_cast<int>(s);
-      return true;
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      return false;
-    }
-  }
-
-
-
   void DatabaseWrapper::AddAttachment(int64_t id,
                                       const FileInfo& attachment)
   {
@@ -470,10 +423,10 @@
 
 
 
-  void DatabaseWrapper::ListAvailableAttachments(std::list<FileContentType>& result,
+  void DatabaseWrapper::ListAvailableAttachments(std::list<FileContentType>& target,
                                                  int64_t id)
   {
-    result.clear();
+    target.clear();
 
     SQLite::Statement s(db_, SQLITE_FROM_HERE, 
                         "SELECT fileType FROM AttachedFiles WHERE id=?");
@@ -481,7 +434,7 @@
 
     while (s.Step())
     {
-      result.push_back(static_cast<FileContentType>(s.ColumnInt(0)));
+      target.push_back(static_cast<FileContentType>(s.ColumnInt(0)));
     }
   }
 
@@ -511,18 +464,33 @@
     }
   }
 
-  void DatabaseWrapper::SetMainDicomTags(int64_t id,
-                                         const DicomMap& tags)
+
+  static void SetMainDicomTagsInternal(SQLite::Statement& s,
+                                       int64_t id,
+                                       const DicomTag& tag,
+                                       const std::string& value)
   {
-    DicomArray flattened(tags);
-    for (size_t i = 0; i < flattened.GetSize(); i++)
+    s.BindInt64(0, id);
+    s.BindInt(1, tag.GetGroup());
+    s.BindInt(2, tag.GetElement());
+    s.BindString(3, value);
+    s.Run();
+  }
+
+
+  void DatabaseWrapper::SetMainDicomTag(int64_t id,
+                                        const DicomTag& tag,
+                                        const std::string& value)
+  {
+    if (tag.IsIdentifier())
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO DicomIdentifiers VALUES(?, ?, ?, ?)");
+      SetMainDicomTagsInternal(s, id, tag, value);
+    }
+    else
     {
       SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)");
-      s.BindInt64(0, id);
-      s.BindInt(1, flattened.GetElement(i).GetTag().GetGroup());
-      s.BindInt(2, flattened.GetElement(i).GetTag().GetElement());
-      s.BindString(3, flattened.GetElement(i).GetValue().AsString());
-      s.Run();
+      SetMainDicomTagsInternal(s, id, tag, value);
     }
   }
 
@@ -539,10 +507,19 @@
                    s.ColumnInt(2),
                    s.ColumnString(3));
     }
+
+    SQLite::Statement s2(db_, SQLITE_FROM_HERE, "SELECT * FROM DicomIdentifiers WHERE id=?");
+    s2.BindInt64(0, id);
+    while (s2.Step())
+    {
+      map.SetValue(s2.ColumnInt(1),
+                   s2.ColumnInt(2),
+                   s2.ColumnString(3));
+    }
   }
 
 
-  bool DatabaseWrapper::GetParentPublicId(std::string& result,
+  bool DatabaseWrapper::GetParentPublicId(std::string& target,
                                           int64_t id)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b "
@@ -551,7 +528,7 @@
 
     if (s.Step())
     {
-      result = s.ColumnString(0);
+      target = s.ColumnString(0);
       return true;
     }
     else
@@ -561,201 +538,159 @@
   }
 
 
-  void DatabaseWrapper::GetChildrenPublicId(std::list<std::string>& result,
+  void DatabaseWrapper::GetChildrenPublicId(std::list<std::string>& target,
                                             int64_t id)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b  "
                         "WHERE a.parentId = b.internalId AND b.internalId = ?");     
     s.BindInt64(0, id);
 
-    result.clear();
+    target.clear();
 
     while (s.Step())
     {
-      result.push_back(s.ColumnString(0));
+      target.push_back(s.ColumnString(0));
     }
   }
 
 
-  void DatabaseWrapper::GetChildrenInternalId(std::list<int64_t>& result,
+  void DatabaseWrapper::GetChildrenInternalId(std::list<int64_t>& target,
                                               int64_t id)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.internalId FROM Resources AS a, Resources AS b  "
                         "WHERE a.parentId = b.internalId AND b.internalId = ?");     
     s.BindInt64(0, id);
 
-    result.clear();
+    target.clear();
 
     while (s.Step())
     {
-      result.push_back(s.ColumnInt64(0));
+      target.push_back(s.ColumnInt64(0));
     }
   }
 
 
-  void DatabaseWrapper::LogChange(ChangeType changeType,
-                                  int64_t internalId,
-                                  ResourceType resourceType,
-                                  const boost::posix_time::ptime& date)
+  void DatabaseWrapper::LogChange(int64_t internalId,
+                                  const ServerIndexChange& change)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Changes VALUES(NULL, ?, ?, ?, ?)");
-    s.BindInt(0, changeType);
+    s.BindInt(0, change.GetChangeType());
     s.BindInt64(1, internalId);
-    s.BindInt(2, resourceType);
-    s.BindString(3, boost::posix_time::to_iso_string(date));
+    s.BindInt(2, change.GetResourceType());
+    s.BindString(3, change.GetDate());
+    s.Run();
+  }
+
+
+  void DatabaseWrapper::GetChangesInternal(std::list<ServerIndexChange>& target,
+                                           bool& done,
+                                           SQLite::Statement& s,
+                                           uint32_t maxResults)
+  {
+    target.clear();
+
+    while (target.size() < maxResults && s.Step())
+    {
+      int64_t seq = s.ColumnInt64(0);
+      ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
+      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
+      const std::string& date = s.ColumnString(4);
+
+      int64_t internalId = s.ColumnInt64(2);
+      std::string publicId = GetPublicId(internalId);
+
+      target.push_back(ServerIndexChange(seq, changeType, resourceType, publicId, date));
+    }
+
+    done = !(target.size() == maxResults && s.Step());
+  }
+
+
+  void DatabaseWrapper::GetChanges(std::list<ServerIndexChange>& target,
+                                   bool& done,
+                                   int64_t since,
+                                   uint32_t maxResults)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
+    s.BindInt64(0, since);
+    s.BindInt(1, maxResults + 1);
+    GetChangesInternal(target, done, s, maxResults);
+  }
+
+  void DatabaseWrapper::GetLastChange(std::list<ServerIndexChange>& target)
+  {
+    bool done;  // Ignored
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
+    GetChangesInternal(target, done, s, 1);
+  }
+
+
+  void DatabaseWrapper::LogExportedResource(const ExportedResource& resource)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
+
+    s.BindInt(0, resource.GetResourceType());
+    s.BindString(1, resource.GetPublicId());
+    s.BindString(2, resource.GetModality());
+    s.BindString(3, resource.GetPatientId());
+    s.BindString(4, resource.GetStudyInstanceUid());
+    s.BindString(5, resource.GetSeriesInstanceUid());
+    s.BindString(6, resource.GetSopInstanceUid());
+    s.BindString(7, resource.GetDate());
     s.Run();      
   }
 
 
-  void DatabaseWrapper::GetChangesInternal(Json::Value& target,
-                                           SQLite::Statement& s,
-                                           int64_t since,
-                                           unsigned int maxResults)
-  {
-    Json::Value changes = Json::arrayValue;
-    int64_t last = since;
-
-    while (changes.size() < maxResults && s.Step())
-    {
-      int64_t seq = s.ColumnInt64(0);
-      ChangeType changeType = static_cast<ChangeType>(s.ColumnInt(1));
-      int64_t internalId = s.ColumnInt64(2);
-      ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(3));
-      const std::string& date = s.ColumnString(4);
-      std::string publicId = GetPublicId(internalId);
-
-      Json::Value item = Json::objectValue;
-      item["Seq"] = static_cast<int>(seq);
-      item["ChangeType"] = EnumerationToString(changeType);
-      item["ResourceType"] = EnumerationToString(resourceType);
-      item["ID"] = publicId;
-      item["Path"] = GetBasePath(resourceType, publicId);
-      item["Date"] = date;
-      last = seq;
-
-      changes.append(item);
-    }
-
-    target = Json::objectValue;
-    target["Changes"] = changes;
-    target["Done"] = !(changes.size() == maxResults && s.Step());
-    target["Last"] = static_cast<int>(last);
-  }
-
-
-  void DatabaseWrapper::GetChanges(Json::Value& target,
-                                   int64_t since,
-                                   unsigned int maxResults)
+  void DatabaseWrapper::GetExportedResourcesInternal(std::list<ExportedResource>& target,
+                                                     bool& done,
+                                                     SQLite::Statement& s,
+                                                     uint32_t maxResults)
   {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes WHERE seq>? ORDER BY seq LIMIT ?");
-    s.BindInt64(0, since);
-    s.BindInt(1, maxResults + 1);
-    GetChangesInternal(target, s, since, maxResults);
-  }
-
-  void DatabaseWrapper::GetLastChange(Json::Value& target)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
-    GetChangesInternal(target, s, 0, 1);
-  }
-
+    target.clear();
 
-  void DatabaseWrapper::LogExportedResource(ResourceType resourceType,
-                                            const std::string& publicId,
-                                            const std::string& remoteModality,
-                                            const std::string& patientId,
-                                            const std::string& studyInstanceUid,
-                                            const std::string& seriesInstanceUid,
-                                            const std::string& sopInstanceUid,
-                                            const boost::posix_time::ptime& date)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "INSERT INTO ExportedResources VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?)");
-
-    s.BindInt(0, resourceType);
-    s.BindString(1, publicId);
-    s.BindString(2, remoteModality);
-    s.BindString(3, patientId);
-    s.BindString(4, studyInstanceUid);
-    s.BindString(5, seriesInstanceUid);
-    s.BindString(6, sopInstanceUid);
-    s.BindString(7, boost::posix_time::to_iso_string(date));
-
-    s.Run();      
-  }
-
-
-  void DatabaseWrapper::GetExportedResourcesInternal(Json::Value& target,
-                                                     SQLite::Statement& s,
-                                                     int64_t since,
-                                                     unsigned int maxResults)
-  {
-    Json::Value changes = Json::arrayValue;
-    int64_t last = since;
-
-    while (changes.size() < maxResults && s.Step())
+    while (target.size() < maxResults && s.Step())
     {
       int64_t seq = s.ColumnInt64(0);
       ResourceType resourceType = static_cast<ResourceType>(s.ColumnInt(1));
       std::string publicId = s.ColumnString(2);
 
-      Json::Value item = Json::objectValue;
-      item["Seq"] = static_cast<int>(seq);
-      item["ResourceType"] = EnumerationToString(resourceType);
-      item["ID"] = publicId;
-      item["Path"] = GetBasePath(resourceType, publicId);
-      item["RemoteModality"] = s.ColumnString(3);
-      item["Date"] = s.ColumnString(8);
-
-      // WARNING: Do not add "break" below and do not reorder the case items!
-      switch (resourceType)
-      {
-      case ResourceType_Instance:
-        item["SopInstanceUid"] = s.ColumnString(7);
+      ExportedResource resource(seq, 
+                                resourceType,
+                                publicId,
+                                s.ColumnString(3),  // modality
+                                s.ColumnString(8),  // date
+                                s.ColumnString(4),  // patient ID
+                                s.ColumnString(5),  // study instance UID
+                                s.ColumnString(6),  // series instance UID
+                                s.ColumnString(7)); // sop instance UID
 
-      case ResourceType_Series:
-        item["SeriesInstanceUid"] = s.ColumnString(6);
-
-      case ResourceType_Study:
-        item["StudyInstanceUid"] = s.ColumnString(5);
-
-      case ResourceType_Patient:
-        item["PatientId"] = s.ColumnString(4);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      last = seq;
-
-      changes.append(item);
+      target.push_back(resource);
     }
 
-    target = Json::objectValue;
-    target["Exports"] = changes;
-    target["Done"] = !(changes.size() == maxResults && s.Step());
-    target["Last"] = static_cast<int>(last);
+    done = !(target.size() == maxResults && s.Step());
   }
 
 
-  void DatabaseWrapper::GetExportedResources(Json::Value& target,
+  void DatabaseWrapper::GetExportedResources(std::list<ExportedResource>& target,
+                                             bool& done,
                                              int64_t since,
-                                             unsigned int maxResults)
+                                             uint32_t maxResults)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, 
                         "SELECT * FROM ExportedResources WHERE seq>? ORDER BY seq LIMIT ?");
     s.BindInt64(0, since);
     s.BindInt(1, maxResults + 1);
-    GetExportedResourcesInternal(target, s, since, maxResults);
+    GetExportedResourcesInternal(target, done, s, maxResults);
   }
 
     
-  void DatabaseWrapper::GetLastExportedResource(Json::Value& target)
+  void DatabaseWrapper::GetLastExportedResource(std::list<ExportedResource>& target)
   {
+    bool done;  // Ignored
     SQLite::Statement s(db_, SQLITE_FROM_HERE, 
                         "SELECT * FROM ExportedResources ORDER BY seq DESC LIMIT 1");
-    GetExportedResourcesInternal(target, s, 0, 1);
+    GetExportedResourcesInternal(target, done, s, 1);
   }
 
 
@@ -794,30 +729,37 @@
     return static_cast<uint64_t>(s.ColumnInt64(0));
   }
 
-  void DatabaseWrapper::GetAllPublicIds(Json::Value& target,
+  void DatabaseWrapper::GetAllPublicIds(std::list<std::string>& target,
                                         ResourceType resourceType)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE resourceType=?");
     s.BindInt(0, resourceType);
 
-    target = Json::arrayValue;
+    target.clear();
     while (s.Step())
     {
-      target.append(s.ColumnString(0));
+      target.push_back(s.ColumnString(0));
     }
   }
 
+  static void UpgradeDatabase(SQLite::Connection& db,
+                              EmbeddedResources::FileResourceId script)
+  {
+    std::string upgrade;
+    EmbeddedResources::GetFileResource(upgrade, script);
+    db.BeginTransaction();
+    db.Execute(upgrade);
+    db.CommitTransaction();    
+  }
 
-  DatabaseWrapper::DatabaseWrapper(const std::string& path,
-                                   IServerIndexListener& listener) :
-    listener_(listener)
+
+  DatabaseWrapper::DatabaseWrapper(const std::string& path) : listener_(NULL)
   {
     db_.Open(path);
     Open();
   }
 
-  DatabaseWrapper::DatabaseWrapper(IServerIndexListener& listener) :
-    listener_(listener)
+  DatabaseWrapper::DatabaseWrapper() : listener_(NULL)
   {
     db_.OpenInMemory();
     Open();
@@ -842,7 +784,12 @@
     }
 
     // Check the version of the database
-    std::string version = GetGlobalProperty(GlobalProperty_DatabaseSchemaVersion, "Unknown");
+    std::string version;
+    if (!LookupGlobalProperty(version, GlobalProperty_DatabaseSchemaVersion))
+    {
+      version = "Unknown";
+    }
+
     bool ok = false;
     try
     {
@@ -851,25 +798,33 @@
 
       /**
        * History of the database versions:
+       *  - Orthanc before Orthanc 0.3.0 (inclusive) had no version
+       *  - Version 2: only Orthanc 0.3.1
        *  - Version 3: from Orthanc 0.3.2 to Orthanc 0.7.2 (inclusive)
-       *  - Version 4: from Orthanc 0.7.3 (inclusive)
+       *  - Version 4: from Orthanc 0.7.3 to Orthanc 0.8.4 (inclusive)
+       *  - Version 5: from Orthanc 0.8.5 (inclusive)
        **/
 
-      // This version of Orthanc is only compatible with versions 3 of 4 of the DB schema
-      ok = (v == 3 || v == 4);
+      // This version of Orthanc is only compatible with versions 3, 4 and 5 of the DB schema
+      ok = (v == 3 || v == 4 || v == 5);
 
       if (v == 3)
       {
         LOG(WARNING) << "Upgrading database version from 3 to 4";
-        std::string upgrade;
-        EmbeddedResources::GetFileResource(upgrade, EmbeddedResources::UPGRADE_DATABASE_3_TO_4);
-        db_.BeginTransaction();
-        db_.Execute(upgrade);
-        db_.CommitTransaction();
+        UpgradeDatabase(db_, EmbeddedResources::UPGRADE_DATABASE_3_TO_4);
+        v = 4;
+      }
+
+      if (v == 4)
+      {
+        LOG(WARNING) << "Upgrading database version from 4 to 5";
+        UpgradeDatabase(db_, EmbeddedResources::UPGRADE_DATABASE_4_TO_5);
+        v = 5;
       }
     }
     catch (boost::bad_lexical_cast&)
     {
+      ok = false;
     }
 
     if (!ok)
@@ -880,7 +835,13 @@
 
     signalRemainingAncestor_ = new Internals::SignalRemainingAncestor;
     db_.Register(signalRemainingAncestor_);
-    db_.Register(new Internals::SignalFileDeleted(listener_));
+  }
+
+  void DatabaseWrapper::SetListener(IServerIndexListener& listener)
+  {
+    listener_ = &listener;
+    db_.Register(new Internals::SignalFileDeleted(listener));
+    db_.Register(new Internals::SignalResourceDeleted(listener));
   }
 
   uint64_t DatabaseWrapper::GetResourceCount(ResourceType resourceType)
@@ -967,33 +928,6 @@
   }
 
 
-  uint64_t DatabaseWrapper::IncrementGlobalSequence(GlobalProperty property)
-  {
-    std::string oldValue;
-
-    if (LookupGlobalProperty(oldValue, property))
-    {
-      uint64_t oldNumber;
-
-      try
-      {
-        oldNumber = boost::lexical_cast<uint64_t>(oldValue);
-        SetGlobalProperty(property, boost::lexical_cast<std::string>(oldNumber + 1));
-        return oldNumber + 1;
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-    else
-    {
-      // Initialize the sequence at "1"
-      SetGlobalProperty(property, "1");
-      return 1;
-    }
-  }
-
 
   void DatabaseWrapper::ClearTable(const std::string& tableName)
   {
@@ -1010,39 +944,61 @@
   }
 
 
-  void  DatabaseWrapper::LookupTagValue(std::list<int64_t>& result,
-                                        DicomTag tag,
-                                        const std::string& value)
+  void  DatabaseWrapper::LookupIdentifier(std::list<int64_t>& target,
+                                          const DicomTag& tag,
+                                          const std::string& value)
   {
+    if (!tag.IsIdentifier())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
     SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT id FROM MainDicomTags WHERE tagGroup=? AND tagElement=? and value=?");
+                        "SELECT id FROM DicomIdentifiers WHERE tagGroup=? AND tagElement=? and value=?");
 
     s.BindInt(0, tag.GetGroup());
     s.BindInt(1, tag.GetElement());
     s.BindString(2, value);
 
-    result.clear();
+    target.clear();
 
     while (s.Step())
     {
-      result.push_back(s.ColumnInt64(0));
+      target.push_back(s.ColumnInt64(0));
     }
   }
 
 
-  void  DatabaseWrapper::LookupTagValue(std::list<int64_t>& result,
-                                        const std::string& value)
+  void  DatabaseWrapper::LookupIdentifier(std::list<int64_t>& target,
+                                          const std::string& value)
   {
     SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT id FROM MainDicomTags WHERE value=?");
+                        "SELECT id FROM DicomIdentifiers WHERE value=?");
 
     s.BindString(0, value);
 
-    result.clear();
+    target.clear();
 
     while (s.Step())
     {
-      result.push_back(s.ColumnInt64(0));
+      target.push_back(s.ColumnInt64(0));
     }
   }
+
+
+  void DatabaseWrapper::GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                       int64_t id)
+  {
+    target.clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?");
+    s.BindInt64(0, id);
+
+    while (s.Step())
+    {
+      MetadataType key = static_cast<MetadataType>(s.ColumnInt(0));
+      target[key] = s.ColumnString(1);
+    }
+  }
+
 }
--- a/OrthancServer/DatabaseWrapper.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DatabaseWrapper.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,14 +32,10 @@
 
 #pragma once
 
+#include "IDatabaseWrapper.h"
+
 #include "../Core/SQLite/Connection.h"
 #include "../Core/SQLite/Transaction.h"
-#include "../Core/DicomFormat/DicomInstanceHasher.h"
-#include "../Core/FileStorage/FileInfo.h"
-#include "IServerIndexListener.h"
-
-#include <list>
-#include <boost/date_time/posix_time/posix_time.hpp>
 
 namespace Orthanc
 {
@@ -53,187 +49,193 @@
    * translates low-level requests into SQL statements. Mutual
    * exclusion MUST be implemented at a higher level.
    **/
-  class DatabaseWrapper
+  class DatabaseWrapper : public IDatabaseWrapper
   {
   private:
-    IServerIndexListener& listener_;
+    IServerIndexListener* listener_;
     SQLite::Connection db_;
     Internals::SignalRemainingAncestor* signalRemainingAncestor_;
 
     void Open();
 
-    void GetChangesInternal(Json::Value& target,
+    void GetChangesInternal(std::list<ServerIndexChange>& target,
+                            bool& done,
                             SQLite::Statement& s,
-                            int64_t since,
-                            unsigned int maxResults);
+                            uint32_t maxResults);
 
-    void GetExportedResourcesInternal(Json::Value& target,
+    void GetExportedResourcesInternal(std::list<ExportedResource>& target,
+                                      bool& done,
                                       SQLite::Statement& s,
-                                      int64_t since,
-                                      unsigned int maxResults);
+                                      uint32_t maxResults);
+
+    void ClearTable(const std::string& tableName);
 
   public:
-    void SetGlobalProperty(GlobalProperty property,
-                           const std::string& value);
+    DatabaseWrapper(const std::string& path);
 
-    bool LookupGlobalProperty(std::string& target,
-                              GlobalProperty property);
+    DatabaseWrapper();
+
+    virtual void SetListener(IServerIndexListener& listener);
 
-    std::string GetGlobalProperty(GlobalProperty property,
-                                  const std::string& defaultValue = "");
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   const std::string& value);
 
-    int64_t CreateResource(const std::string& publicId,
-                           ResourceType type);
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property);
+
+    virtual int64_t CreateResource(const std::string& publicId,
+                                   ResourceType type);
 
-    bool LookupResource(const std::string& publicId,
-                        int64_t& id,
-                        ResourceType& type);
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId);
 
-    bool LookupParent(int64_t& parentId,
-                      int64_t resourceId);
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId);
 
-    std::string GetPublicId(int64_t resourceId);
+    virtual std::string GetPublicId(int64_t resourceId);
 
-    ResourceType GetResourceType(int64_t resourceId);
+    virtual ResourceType GetResourceType(int64_t resourceId);
 
-    void AttachChild(int64_t parent,
-                     int64_t child);
+    virtual void AttachChild(int64_t parent,
+                             int64_t child);
 
-    void GetChildren(Json::Value& childrenPublicIds,
-                     int64_t id);
+    virtual void DeleteResource(int64_t id);
 
-    void DeleteResource(int64_t id);
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value);
 
-    void SetMetadata(int64_t id,
-                     MetadataType type,
-                     const std::string& value);
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type);
 
-    void DeleteMetadata(int64_t id,
-                        MetadataType type);
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type);
 
-    bool LookupMetadata(std::string& target,
-                        int64_t id,
-                        MetadataType type);
-
-    void ListAvailableMetadata(std::list<MetadataType>& target,
-                               int64_t id);
+    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
+                                       int64_t id);
 
-    std::string GetMetadata(int64_t id,
-                            MetadataType type,
-                            const std::string& defaultValue = "");
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment);
 
-    bool GetMetadataAsInteger(int& result,
-                              int64_t id,
-                              MetadataType type);
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment);
+
+    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+                                          int64_t id);
 
-    void AddAttachment(int64_t id,
-                       const FileInfo& attachment);
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t id,
+                                  FileContentType contentType);
 
-    void DeleteAttachment(int64_t id,
-                          FileContentType attachment);
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value);
 
-    void ListAvailableAttachments(std::list<FileContentType>& result,
+    virtual void GetMainDicomTags(DicomMap& map,
                                   int64_t id);
 
-    bool LookupAttachment(FileInfo& attachment,
-                          int64_t id,
-                          FileContentType contentType);
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id);
 
-    void SetMainDicomTags(int64_t id,
-                          const DicomMap& tags);
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id);
 
-    void GetMainDicomTags(DicomMap& map,
-                          int64_t id);
-
-    bool GetParentPublicId(std::string& result,
-                           int64_t id);
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change);
 
-    void GetChildrenPublicId(std::list<std::string>& result,
-                             int64_t id);
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults);
 
-    void GetChildrenInternalId(std::list<int64_t>& result,
-                               int64_t id);
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/);
 
-    void LogChange(ChangeType changeType,
-                   int64_t internalId,
-                   ResourceType resourceType,
-                   const boost::posix_time::ptime& date = boost::posix_time::second_clock::local_time());
-
-    void GetChanges(Json::Value& target,
-                    int64_t since,
-                    unsigned int maxResults);
-
-    void GetLastChange(Json::Value& target);
+    virtual void LogExportedResource(const ExportedResource& resource);
+    
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults);
 
-    void LogExportedResource(ResourceType resourceType,
-                             const std::string& publicId,
-                             const std::string& remoteModality,
-                             const std::string& patientId,
-                             const std::string& studyInstanceUid,
-                             const std::string& seriesInstanceUid,
-                             const std::string& sopInstanceUid,
-                             const boost::posix_time::ptime& date = 
-                             boost::posix_time::second_clock::local_time());
-    
-    void GetExportedResources(Json::Value& target,
-                              int64_t since,
-                              unsigned int maxResults);
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/);
 
-    void GetLastExportedResource(Json::Value& target);
-
-    // For unit testing only!
-    int64_t GetTableRecordCount(const std::string& table);
-    
-    uint64_t GetTotalCompressedSize();
+    virtual uint64_t GetTotalCompressedSize();
     
-    uint64_t GetTotalUncompressedSize();
+    virtual uint64_t GetTotalUncompressedSize();
 
-    uint64_t GetResourceCount(ResourceType resourceType);
+    virtual uint64_t GetResourceCount(ResourceType resourceType);
 
-    void GetAllPublicIds(Json::Value& target,
-                         ResourceType resourceType);
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType);
 
-    bool SelectPatientToRecycle(int64_t& internalId);
-
-    bool SelectPatientToRecycle(int64_t& internalId,
-                                int64_t patientIdToAvoid);
+    virtual bool SelectPatientToRecycle(int64_t& internalId);
 
-    bool IsProtectedPatient(int64_t internalId);
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid);
 
-    void SetProtectedPatient(int64_t internalId, 
-                             bool isProtected);
+    virtual bool IsProtectedPatient(int64_t internalId);
 
-    DatabaseWrapper(const std::string& path,
-                    IServerIndexListener& listener);
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected);
 
-    DatabaseWrapper(IServerIndexListener& listener);
-
-    SQLite::Transaction* StartTransaction()
+    virtual SQLite::ITransaction* StartTransaction()
     {
       return new SQLite::Transaction(db_);
     }
 
+    virtual void FlushToDisk()
+    {
+      db_.FlushToDisk();
+    }
+
+    virtual bool HasFlushToDisk() const
+    {
+      return true;
+    }
+
+    virtual void ClearChanges()
+    {
+      ClearTable("Changes");
+    }
+
+    virtual void ClearExportedResources()
+    {
+      ClearTable("ExportedResources");
+    }
+
+    virtual bool IsExistingResource(int64_t internalId);
+
+    virtual void LookupIdentifier(std::list<int64_t>& target,
+                                  const DicomTag& tag,
+                                  const std::string& value);
+
+    virtual void LookupIdentifier(std::list<int64_t>& target,
+                                  const std::string& value);
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id);
+
+
+
+
+    /**
+     * The methods declared below are for unit testing only!
+     **/
+
     const char* GetErrorMessage() const
     {
       return db_.GetErrorMessage();
     }
 
-    void FlushToDisk()
-    {
-      db_.FlushToDisk();
-    }
-
-    uint64_t IncrementGlobalSequence(GlobalProperty property);
-
-    void ClearTable(const std::string& tableName);
+    void GetChildren(std::list<std::string>& childrenPublicIds,
+                     int64_t id);
 
-    bool IsExistingResource(int64_t internalId);
+    int64_t GetTableRecordCount(const std::string& table);
+    
+    bool GetParentPublicId(std::string& target,
+                           int64_t id);
 
-    void LookupTagValue(std::list<int64_t>& result,
-                        DicomTag tag,
-                        const std::string& value);
-
-    void LookupTagValue(std::list<int64_t>& result,
-                        const std::string& value);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomDirWriter.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,560 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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/>.
+ **/
+
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+Copyright (C) 1994-2011, OFFIS e.V.
+All rights reserved.
+
+This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+- Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=========================================================================*/
+
+
+
+/***
+    
+    Validation:
+
+    # sudo apt-get install dicom3tools
+    # dciodvfy DICOMDIR 2>&1 | less
+    # dcentvfy DICOMDIR 2>&1 | less
+
+    http://www.dclunie.com/dicom3tools/dciodvfy.html
+
+    DICOMDIR viewer working with Wine under Linux:
+    http://www.microdicom.com/
+
+ ***/
+
+
+#include "PrecompiledHeadersServer.h"
+#include "DicomDirWriter.h"
+
+#include "FromDcmtkBridge.h"
+#include "ToDcmtkBridge.h"
+
+#include "../Core/OrthancException.h"
+#include "../Core/Uuid.h"
+
+#include <dcmtk/dcmdata/dcdicdir.h>
+#include <dcmtk/dcmdata/dcmetinf.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcuid.h>
+#include <dcmtk/dcmdata/dcddirif.h>
+#include <dcmtk/dcmdata/dcvrui.h>
+#include <dcmtk/dcmdata/dcsequen.h>
+#include <dcmtk/dcmdata/dcostrmf.h>
+#include "dcmtk/dcmdata/dcvrda.h"     /* for class DcmDate */
+#include "dcmtk/dcmdata/dcvrtm.h"     /* for class DcmTime */
+
+#include <memory>
+#include <glog/logging.h>
+
+namespace Orthanc
+{
+  class DicomDirWriter::PImpl
+  {
+  private:
+    std::string fileSetId_;
+    Toolbox::TemporaryFile file_;
+    std::auto_ptr<DcmDicomDir> dir_;
+
+    typedef std::pair<ResourceType, std::string>  IndexKey;
+    typedef std::map<IndexKey, DcmDirectoryRecord* >  Index;
+    Index  index_;
+
+
+    /*******************************************************************************
+     * Functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0
+     *******************************************************************************/
+
+    // print an error message to the console (stderr) that something went wrong with an attribute
+    static void printAttributeErrorMessage(const DcmTagKey &key,
+                                           const OFCondition &error,
+                                           const char *operation)
+    {
+      if (error.bad())
+      {
+        OFString str;
+        if (operation != NULL)
+        {
+          str = "cannot ";
+          str += operation;
+          str += " ";
+        }
+        LOG(ERROR) << error.text() << ": " << str << DcmTag(key).getTagName() << " " << key;
+      }
+    }
+
+    // copy element from dataset to directory record
+    static void copyElement(DcmItem& dataset,
+                            const DcmTagKey &key,
+                            DcmDirectoryRecord& record,
+                            const OFBool optional,
+                            const OFBool copyEmpty)
+    {
+      /* check whether tag exists in source dataset (if optional) */
+      if (!optional || (copyEmpty && dataset.tagExists(key)) || dataset.tagExistsWithValue(key))
+      {
+        DcmElement *delem = NULL;
+        /* get copy of element from source dataset */
+        OFCondition status = dataset.findAndGetElement(key, delem, OFFalse /*searchIntoSub*/, OFTrue /*createCopy*/);
+        if (status.good())
+        {
+          /* ... and insert it into the destination dataset (record) */
+          status = record.insert(delem, OFTrue /*replaceOld*/);
+          if (status.good())
+          {
+            DcmTag tag(key);
+            /* check for correct VR in the dataset */
+            if (delem->getVR() != tag.getEVR())
+            {
+              /* create warning message */
+              LOG(WARNING) << "DICOMDIR: possibly wrong VR: "
+                           << tag.getTagName() << " " << key << " with "
+                           << DcmVR(delem->getVR()).getVRName() << " found, expected "
+                           << tag.getVRName() << " instead";
+            }
+          } else
+            delete delem;
+        } else if (status == EC_TagNotFound)
+          status = record.insertEmptyElement(key);
+        printAttributeErrorMessage(key, status, "insert");
+      }
+    }
+
+    // copy optional string value from dataset to directory record
+    static void copyStringWithDefault(DcmItem& dataset,
+                                      const DcmTagKey &key,
+                                      DcmDirectoryRecord& record,
+                                      const char *defaultValue,
+                                      const OFBool printWarning)
+    {
+        OFCondition status;
+        if (dataset.tagExistsWithValue(key))
+        {
+          OFString stringValue;
+          /* retrieve string value from source dataset and put it into the destination dataset */
+          status = dataset.findAndGetOFStringArray(key, stringValue);
+          if (status.good())
+            status = record.putAndInsertString(key, stringValue.c_str());
+        } else {
+          if (printWarning && (defaultValue != NULL))
+          {
+            /* create warning message */
+            LOG(WARNING) << "DICOMDIR: " << DcmTag(key).getTagName() << " "
+                         << key << " missing, using alternative: " << defaultValue;
+          }
+          /* put default value */
+          status = record.putAndInsertString(key, defaultValue);
+        }
+    }
+
+    // create alternative study date if absent in dataset
+    static OFString &alternativeStudyDate(DcmItem& dataset,
+                                          OFString &result)
+    {
+      /* use another date if present */
+      if (dataset.findAndGetOFStringArray(DCM_SeriesDate, result).bad() || result.empty())
+      {
+        if (dataset.findAndGetOFStringArray(DCM_AcquisitionDate, result).bad() || result.empty())
+        {
+          if (dataset.findAndGetOFStringArray(DCM_ContentDate, result).bad() || result.empty())
+          {
+            /* use current date, "19000101" in case of error */
+            DcmDate::getCurrentDate(result);
+          }
+        }
+      }
+      return result;
+    }
+
+
+    // create alternative study time if absent in dataset
+    static OFString &alternativeStudyTime(DcmItem& dataset,
+                                          OFString &result)
+    {
+      /* use another time if present */
+      if (dataset.findAndGetOFStringArray(DCM_SeriesTime, result).bad() || result.empty())
+      {
+        if (dataset.findAndGetOFStringArray(DCM_AcquisitionTime, result).bad() || result.empty())
+        {
+          if (dataset.findAndGetOFStringArray(DCM_ContentTime, result).bad() || result.empty())
+          {
+            /* use current time, "0000" in case of error */
+            DcmTime::getCurrentTime(result);
+          }
+        }
+      }
+      return result;
+    }
+
+
+    static void copyElementType1(DcmItem& dataset,
+                                 const DcmTagKey &key,
+                                 DcmDirectoryRecord& record)
+    {
+      copyElement(dataset, key, record, OFFalse /*optional*/, OFFalse /*copyEmpty*/);
+    }
+
+    static void copyElementType1C(DcmItem& dataset,
+                                  const DcmTagKey &key,
+                                  DcmDirectoryRecord& record)
+    {
+      copyElement(dataset, key, record, OFTrue /*optional*/, OFFalse /*copyEmpty*/);
+    }
+
+    static void copyElementType2(DcmItem& dataset,
+                                 const DcmTagKey &key,
+                                 DcmDirectoryRecord& record)
+    {
+      copyElement(dataset, key, record, OFFalse /*optional*/, OFTrue /*copyEmpty*/);
+    }
+
+    /*******************************************************************************
+     * End of functions adapted from "dcmdata/libsrc/dcddirif.cc" from DCMTK 3.6.0
+     *******************************************************************************/
+
+
+    DcmDicomDir& GetDicomDir()
+    {
+      if (dir_.get() == NULL)
+      {
+        dir_.reset(new DcmDicomDir(file_.GetPath().c_str(), 
+                                   fileSetId_.c_str()));
+      }
+
+      return *dir_;
+    }
+
+
+    DcmDirectoryRecord& GetRoot()
+    {
+      return GetDicomDir().getRootRecord();
+    }
+
+
+  public:
+    PImpl() : fileSetId_("ORTHANC_MEDIA")
+    {
+    }
+
+    void FillPatient(DcmDirectoryRecord& record,
+                     DcmItem& dicom)
+    {
+      // cf. "DicomDirInterface::buildPatientRecord()"
+
+      copyElementType1C(dicom, DCM_PatientID, record);
+      copyElementType2(dicom, DCM_PatientName, record);
+    }
+
+    void FillStudy(DcmDirectoryRecord& record,
+                   DcmItem& dicom)
+    {
+      // cf. "DicomDirInterface::buildStudyRecord()"
+
+      OFString tmpString;
+      /* copy attribute values from dataset to study record */
+      copyStringWithDefault(dicom, DCM_StudyDate, record, 
+                            alternativeStudyDate(dicom, tmpString).c_str(), OFTrue /*printWarning*/);
+      copyStringWithDefault(dicom, DCM_StudyTime, record, 
+                            alternativeStudyTime(dicom, tmpString).c_str(), OFTrue /*printWarning*/);
+      copyElementType2(dicom, DCM_StudyDescription, record);
+      copyElementType1(dicom, DCM_StudyInstanceUID, record);
+      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
+      copyElementType1C(dicom, DCM_StudyID, record);
+      copyElementType2(dicom, DCM_AccessionNumber, record);
+    }
+
+    void FillSeries(DcmDirectoryRecord& record,
+                    DcmItem& dicom)
+    {
+      // cf. "DicomDirInterface::buildSeriesRecord()"
+
+      /* copy attribute values from dataset to series record */
+      copyElementType1(dicom, DCM_Modality, record);
+      copyElementType1(dicom, DCM_SeriesInstanceUID, record);
+      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
+      copyElementType1C(dicom, DCM_SeriesNumber, record);
+    }
+
+    void FillInstance(DcmDirectoryRecord& record,
+                      DcmItem& dicom,
+                      DcmMetaInfo& metaInfo,
+                      const char* path)
+    {
+      // cf. "DicomDirInterface::buildImageRecord()"
+
+      /* copy attribute values from dataset to image record */
+      copyElementType1(dicom, DCM_InstanceNumber, record);
+      //copyElementType1C(dicom, DCM_ImageType, record);
+      copyElementType1C(dicom, DCM_ReferencedImageSequence, record);
+
+      OFString tmp;
+
+      DcmElement* item = record.remove(DCM_ReferencedImageSequence);
+      if (item != NULL)
+      {
+        delete item;
+      }
+
+      if (record.putAndInsertString(DCM_ReferencedFileID, path).bad() ||
+          dicom.findAndGetOFStringArray(DCM_SOPClassUID, tmp).bad() ||
+          record.putAndInsertString(DCM_ReferencedSOPClassUIDInFile, tmp.c_str()).bad() ||
+          dicom.findAndGetOFStringArray(DCM_SOPInstanceUID, tmp).bad() ||
+          record.putAndInsertString(DCM_ReferencedSOPInstanceUIDInFile, tmp.c_str()).bad() ||
+          metaInfo.findAndGetOFStringArray(DCM_TransferSyntaxUID, tmp).bad() ||
+          record.putAndInsertString(DCM_ReferencedTransferSyntaxUIDInFile, tmp.c_str()).bad())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
+    
+
+    bool CreateResource(DcmDirectoryRecord*& target,
+                        ResourceType level,
+                        DcmFileFormat& dicom,
+                        const char* filename,
+                        const char* path)
+    {
+      DcmDataset& dataset = *dicom.getDataset();
+
+      OFCondition result;
+      OFString id;
+      E_DirRecType type;
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          result = dataset.findAndGetOFString(DCM_PatientID, id);
+          type = ERT_Patient;
+          break;
+
+        case ResourceType_Study:
+          result = dataset.findAndGetOFString(DCM_StudyInstanceUID, id);
+          type = ERT_Study;
+          break;
+
+        case ResourceType_Series:
+          result = dataset.findAndGetOFString(DCM_SeriesInstanceUID, id);
+          type = ERT_Series;
+          break;
+
+        case ResourceType_Instance:
+          result = dataset.findAndGetOFString(DCM_SOPInstanceUID, id);
+          type = ERT_Image;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (!result.good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      IndexKey key = std::make_pair(level, std::string(id.c_str()));
+      Index::iterator it = index_.find(key);
+
+      if (it != index_.end())
+      {
+        target = it->second;
+        return false; // Already existing
+      }
+
+      std::auto_ptr<DcmDirectoryRecord> record(new DcmDirectoryRecord(type, NULL, filename));
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          FillPatient(*record, dataset);
+          break;
+
+        case ResourceType_Study:
+          FillStudy(*record, dataset);
+          break;
+
+        case ResourceType_Series:
+          FillSeries(*record, dataset);
+          break;
+
+        case ResourceType_Instance:
+          FillInstance(*record, dataset, *dicom.getMetaInfo(), path);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (record->isAffectedBySpecificCharacterSet())
+      {
+        copyElementType1C(dataset, DCM_SpecificCharacterSet, *record);
+      }
+
+      target = record.get();
+      GetRoot().insertSub(record.release());
+      index_[key] = target;
+
+      return true;   // Newly created
+    }
+
+    void Read(std::string& s)
+    {
+      if (!GetDicomDir().write(DICOMDIR_DEFAULT_TRANSFERSYNTAX, 
+                               EET_UndefinedLength /*encodingType*/, 
+                               EGL_withoutGL /*groupLength*/).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      file_.Read(s);
+    }
+
+    void SetFileSetId(const std::string& id)
+    {
+      dir_.reset(NULL);
+      fileSetId_ = id;
+    }
+  };
+
+
+  DicomDirWriter::DicomDirWriter() : pimpl_(new PImpl)
+  {
+  }
+
+  DicomDirWriter::~DicomDirWriter()
+  {
+    if (pimpl_)
+    {
+      delete pimpl_;
+    }
+  }
+
+  void DicomDirWriter::SetFileSetId(const std::string& id)
+  {
+    pimpl_->SetFileSetId(id);
+  }
+
+  void DicomDirWriter::Add(const std::string& directory,
+                           const std::string& filename,
+                           ParsedDicomFile& dicom)
+  {
+    std::string path;
+    if (directory.empty())
+    {
+      path = filename;
+    }
+    else
+    {
+      if (directory[directory.length() - 1] == '/' ||
+          directory[directory.length() - 1] == '\\')
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      path = directory + '\\' + filename;
+    }
+
+    DcmFileFormat& fileFormat = *reinterpret_cast<DcmFileFormat*>(dicom.GetDcmtkObject());
+
+    DcmDirectoryRecord* instance;
+    bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, fileFormat, filename.c_str(), path.c_str());
+    if (isNewInstance)
+    {
+      DcmDirectoryRecord* series;
+      bool isNewSeries = pimpl_->CreateResource(series, ResourceType_Series, fileFormat, filename.c_str(), NULL);
+      series->insertSub(instance);
+
+      if (isNewSeries)
+      {
+        DcmDirectoryRecord* study;
+        bool isNewStudy = pimpl_->CreateResource(study, ResourceType_Study, fileFormat, filename.c_str(), NULL);
+        study->insertSub(series);
+  
+        if (isNewStudy)
+        {
+          DcmDirectoryRecord* patient;
+          pimpl_->CreateResource(patient, ResourceType_Patient, fileFormat, filename.c_str(), NULL);
+          patient->insertSub(study);
+        }
+      }
+    }
+  }
+
+  void DicomDirWriter::Encode(std::string& target)
+  {
+    pimpl_->Read(target);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomDirWriter.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,61 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ParsedDicomFile.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class DicomDirWriter : public boost::noncopyable
+  {
+  private:
+    class PImpl;
+    PImpl* pimpl_;
+
+  public:
+    DicomDirWriter();
+
+    ~DicomDirWriter();
+
+    void SetFileSetId(const std::string& id);
+
+    void Add(const std::string& directory,
+             const std::string& filename,
+             ParsedDicomFile& dicom);
+
+    void Encode(std::string& target);
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomFindQuery.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,360 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "DicomFindQuery.h"
+
+#include "FromDcmtkBridge.h"
+
+#include <boost/regex.hpp> 
+
+
+namespace Orthanc
+{
+  class DicomFindQuery::ValueConstraint : public DicomFindQuery::IConstraint
+  {
+  private:
+    bool          isCaseSensitive_;
+    std::string   expected_;
+
+  public:
+    ValueConstraint(const std::string& value,
+                    bool caseSensitive) :
+      isCaseSensitive_(caseSensitive),
+      expected_(value)
+    {
+    }
+
+    const std::string& GetValue() const
+    {
+      return expected_;
+    }
+
+    virtual bool IsExactConstraint() const
+    {
+      return isCaseSensitive_;
+    }
+
+    virtual bool Apply(const std::string& value) const
+    {
+      if (isCaseSensitive_)
+      {
+        return expected_ == value;
+      }
+      else
+      {
+        std::string v, c;
+        Toolbox::ToLowerCase(v, value);
+        Toolbox::ToLowerCase(c, expected_);
+        return v == c;
+      }
+    }
+  };
+
+
+  class DicomFindQuery::ListConstraint : public DicomFindQuery::IConstraint
+  {
+  private:
+    std::set<std::string>  values_;
+
+  public:
+    ListConstraint(const std::string& values)
+    {
+      std::vector<std::string> items;
+      Toolbox::TokenizeString(items, values, '\\');
+
+      for (size_t i = 0; i < items.size(); i++)
+      {
+        std::string lower;
+        Toolbox::ToLowerCase(lower, items[i]);
+        values_.insert(lower);
+      }
+    }
+
+    virtual bool Apply(const std::string& value) const
+    {
+      std::string tmp;
+      Toolbox::ToLowerCase(tmp, value);
+      return values_.find(tmp) != values_.end();
+    }
+  };
+
+
+  class DicomFindQuery::RangeConstraint : public DicomFindQuery::IConstraint
+  {
+  private:
+    std::string lower_;
+    std::string upper_;
+
+  public:
+    RangeConstraint(const std::string& range)
+    {
+      size_t separator = range.find('-');
+      Toolbox::ToLowerCase(lower_, range.substr(0, separator));
+      Toolbox::ToLowerCase(upper_, range.substr(separator + 1));
+    }
+
+    virtual bool Apply(const std::string& value) const
+    {
+      std::string v;
+      Toolbox::ToLowerCase(v, value);
+
+      if (lower_.size() == 0 && 
+          upper_.size() == 0)
+      {
+        return false;
+      }
+
+      if (lower_.size() == 0)
+      {
+        return v <= upper_;
+      }
+
+      if (upper_.size() == 0)
+      {
+        return v >= lower_;
+      }
+    
+      return (v >= lower_ && v <= upper_);
+    }
+  };
+
+
+  class DicomFindQuery::WildcardConstraint : public DicomFindQuery::IConstraint
+  {
+  private:
+    boost::regex pattern_;
+
+  public:
+    WildcardConstraint(const std::string& wildcard)
+    {
+      pattern_ = boost::regex(Toolbox::WildcardToRegularExpression(wildcard),
+                              boost::regex::icase /* case insensitive search */);
+    }
+
+    virtual bool Apply(const std::string& value) const
+    {
+      return boost::regex_match(value, pattern_);
+    }
+  };
+
+
+  void DicomFindQuery::PrepareMainDicomTags(ResourceType level)
+  {
+    std::set<DicomTag> tags;
+    DicomMap::GetMainDicomTags(tags, level);
+
+    for (std::set<DicomTag>::const_iterator
+           it = tags.begin(); it != tags.end(); ++it)
+    {
+      mainDicomTags_[*it] = level;
+    }
+  }
+
+
+  DicomFindQuery::DicomFindQuery() : 
+    level_(ResourceType_Patient),
+    filterJson_(false)
+  {
+    PrepareMainDicomTags(ResourceType_Patient);
+    PrepareMainDicomTags(ResourceType_Study);
+    PrepareMainDicomTags(ResourceType_Series);
+    PrepareMainDicomTags(ResourceType_Instance);
+  }
+
+
+  DicomFindQuery::~DicomFindQuery()
+  {
+    for (Constraints::iterator it = constraints_.begin();
+         it != constraints_.end(); it++)
+    {
+      delete it->second;
+    }
+  }
+
+
+
+
+  void DicomFindQuery::AssignConstraint(const DicomTag& tag,
+                                        IConstraint* constraint)
+  {
+    Constraints::iterator it = constraints_.find(tag);
+
+    if (it != constraints_.end())
+    {
+      constraints_.erase(it);
+    }
+
+    constraints_[tag] = constraint;
+
+    MainDicomTags::const_iterator tmp = mainDicomTags_.find(tag);
+    if (tmp == mainDicomTags_.end())
+    {
+      // The query depends upon a DICOM tag that is not a main tag
+      // from the point of view of Orthanc, we need to decode the
+      // JSON file on the disk.
+      filterJson_ = true;
+    }
+    else
+    {
+      filteredLevels_.insert(tmp->second);
+    }
+  }
+
+
+  void DicomFindQuery::SetConstraint(const DicomTag& tag,
+                                     const std::string& constraint)
+  {
+    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
+    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
+
+    if (constraint.find('-') != std::string::npos)
+    {
+      AssignConstraint(tag, new RangeConstraint(constraint));
+    }
+    else if (constraint.find('\\') != std::string::npos)
+    {
+      AssignConstraint(tag, new ListConstraint(constraint));
+    }
+    else if (constraint.find('*') != std::string::npos ||
+             constraint.find('?') != std::string::npos)
+    {
+      AssignConstraint(tag, new WildcardConstraint(constraint));
+    }
+    else
+    {
+      /**
+       * Case-insensitive match for PN value representation (Patient
+       * Name). Case-senstive match for all the other value
+       * representations.
+       *
+       * Reference: DICOM PS 3.4
+       *   - C.2.2.2.1 ("Single Value Matching") 
+       *   - C.2.2.2.4 ("Wild Card Matching")
+       * http://medical.nema.org/Dicom/2011/11_04pu.pdf
+       *
+       * "Except for Attributes with a PN Value Representation, only
+       * entities with values which match exactly the value specified in the
+       * request shall match. This matching is case-sensitive, i.e.,
+       * sensitive to the exact encoding of the key attribute value in
+       * character sets where a letter may have multiple encodings (e.g.,
+       * based on its case, its position in a word, or whether it is
+       * accented)
+       * 
+       * For Attributes with a PN Value Representation (e.g., Patient Name
+       * (0010,0010)), an application may perform literal matching that is
+       * either case-sensitive, or that is insensitive to some or all
+       * aspects of case, position, accent, or other character encoding
+       * variants."
+       *
+       * (0008,0018) UI SOPInstanceUID     => Case-sensitive
+       * (0008,0050) SH AccessionNumber    => Case-sensitive
+       * (0010,0020) LO PatientID          => Case-sensitive
+       * (0020,000D) UI StudyInstanceUID   => Case-sensitive
+       * (0020,000E) UI SeriesInstanceUID  => Case-sensitive
+      **/
+
+      AssignConstraint(tag, new ValueConstraint(constraint, FromDcmtkBridge::IsPNValueRepresentation(tag)));
+    }
+  }
+
+
+  bool DicomFindQuery::RestrictIdentifier(std::string& value,
+                                          DicomTag identifier) const
+  {
+    Constraints::const_iterator it = constraints_.find(identifier);
+    if (it == constraints_.end() ||
+        !it->second->IsExactConstraint())
+    {
+      return false;
+    }
+    else
+    {
+      value = dynamic_cast<ValueConstraint*>(it->second)->GetValue();
+      return true;
+    }
+  }
+
+  bool DicomFindQuery::HasMainDicomTagsFilter(ResourceType level) const
+  {
+    return filteredLevels_.find(level) != filteredLevels_.end();
+  }
+
+  bool DicomFindQuery::FilterMainDicomTags(const std::string& resourceId,
+                                           ResourceType level,
+                                           const DicomMap& mainTags) const
+  {
+    std::set<DicomTag> tags;
+    mainTags.GetTags(tags);
+
+    for (std::set<DicomTag>::const_iterator
+           it = tags.begin(); it != tags.end(); ++it)
+    {
+      Constraints::const_iterator constraint = constraints_.find(*it);
+      if (constraint != constraints_.end() &&
+          !constraint->second->Apply(mainTags.GetValue(*it).AsString()))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  bool DicomFindQuery::HasInstanceFilter() const
+  {
+    return filterJson_;
+  }
+
+  bool DicomFindQuery::FilterInstance(const std::string& instanceId,
+                                      const Json::Value& content) const
+  {
+    for (Constraints::const_iterator it = constraints_.begin();
+         it != constraints_.end(); ++it)
+    {
+      std::string tag = it->first.Format();
+      std::string value;
+      if (content.isMember(tag))
+      {
+        value = content.get(tag, Json::arrayValue).get("Value", "").asString();
+      }
+
+      if (!it->second->Apply(value))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomFindQuery.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,110 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ResourceFinder.h"
+
+namespace Orthanc
+{
+  class DicomFindQuery : public ResourceFinder::IQuery
+  {
+  private:
+    class  IConstraint : public boost::noncopyable
+    {
+    public:
+      virtual ~IConstraint()
+      {
+      }
+
+      virtual bool IsExactConstraint() const
+      {
+        return false;
+      }
+
+      virtual bool Apply(const std::string& value) const = 0;
+    };
+
+
+    class ValueConstraint;
+    class RangeConstraint;
+    class ListConstraint;
+    class WildcardConstraint;
+
+    typedef std::map<DicomTag, IConstraint*>  Constraints;
+    typedef std::map<DicomTag, ResourceType>  MainDicomTags;
+
+    MainDicomTags           mainDicomTags_;
+    ResourceType            level_;
+    bool                    filterJson_;
+    Constraints             constraints_;
+    std::set<ResourceType>  filteredLevels_;
+
+    void AssignConstraint(const DicomTag& tag,
+                          IConstraint* constraint);
+
+    void PrepareMainDicomTags(ResourceType level);
+
+
+  public:
+    DicomFindQuery();
+
+    virtual ~DicomFindQuery();
+
+    void SetLevel(ResourceType level)
+    {
+      level_ = level;
+    }
+
+    virtual ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetConstraint(const DicomTag& tag,
+                       const std::string& constraint);
+
+    virtual bool RestrictIdentifier(std::string& value,
+                                    DicomTag identifier) const;
+
+    virtual bool HasMainDicomTagsFilter(ResourceType level) const;
+
+    virtual bool FilterMainDicomTags(const std::string& resourceId,
+                                     ResourceType level,
+                                     const DicomMap& mainTags) const;
+
+    virtual bool HasInstanceFilter() const;
+
+    virtual bool FilterInstance(const std::string& instanceId,
+                                const Json::Value& content) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomInstanceToStore.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,174 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "DicomInstanceToStore.h"
+
+#include "FromDcmtkBridge.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <glog/logging.h>
+
+
+namespace Orthanc
+{
+  static DcmDataset& GetDataset(ParsedDicomFile& file)
+  {
+    return *reinterpret_cast<DcmFileFormat*>(file.GetDcmtkObject())->getDataset();
+  }
+
+
+  void DicomInstanceToStore::AddMetadata(ResourceType level,
+                                         MetadataType metadata,
+                                         const std::string& value)
+  {
+    metadata_[std::make_pair(level, metadata)] = value;
+  }
+
+
+  void DicomInstanceToStore::ComputeMissingInformation()
+  {
+    if (buffer_.HasContent() &&
+        summary_.HasContent() &&
+        json_.HasContent())
+    {
+      // Fine, everything is available
+      return; 
+    }
+    
+    if (!buffer_.HasContent())
+    {
+      if (!parsed_.HasContent())
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+      else
+      {
+        // Serialize the parsed DICOM file
+        buffer_.Allocate();
+        if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), GetDataset(parsed_.GetContent())))
+        {
+          LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer";
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+
+    if (summary_.HasContent() &&
+        json_.HasContent())
+    {
+      return;
+    }
+
+    // At this point, we know that the DICOM file is available as a
+    // memory buffer, but that its summary or its JSON version is
+    // missing
+
+    if (!parsed_.HasContent())
+    {
+      parsed_.TakeOwnership(new ParsedDicomFile(buffer_.GetConstContent()));
+    }
+
+    // At this point, we have parsed the DICOM file
+    
+    if (!summary_.HasContent())
+    {
+      summary_.Allocate();
+      FromDcmtkBridge::Convert(summary_.GetContent(), GetDataset(parsed_.GetContent()));
+    }
+    
+    if (!json_.HasContent())
+    {
+      json_.Allocate();
+      FromDcmtkBridge::ToJson(json_.GetContent(), GetDataset(parsed_.GetContent()));
+    }
+  }
+
+
+
+  const char* DicomInstanceToStore::GetBufferData()
+  {
+    ComputeMissingInformation();
+    
+    if (!buffer_.HasContent())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (buffer_.GetConstContent().size() == 0)
+    {
+      return NULL;
+    }
+    else
+    {
+      return buffer_.GetConstContent().c_str();
+    }
+  }
+
+
+  size_t DicomInstanceToStore::GetBufferSize()
+  {
+    ComputeMissingInformation();
+    
+    if (!buffer_.HasContent())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return buffer_.GetConstContent().size();
+  }
+
+
+  const DicomMap& DicomInstanceToStore::GetSummary()
+  {
+    ComputeMissingInformation();
+    
+    if (!summary_.HasContent())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return summary_.GetConstContent();
+  }
+
+    
+  const Json::Value& DicomInstanceToStore::GetJson()
+  {
+    ComputeMissingInformation();
+    
+    if (!json_.HasContent())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return json_.GetConstContent();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomInstanceToStore.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,215 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ParsedDicomFile.h"
+#include "ServerIndex.h"
+#include "../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  class DicomInstanceToStore
+  {
+  private:
+    template <typename T>
+    class SmartContainer
+    {
+    private:
+      T* content_;
+      bool toDelete_;
+      bool isReadOnly_;
+
+      void Deallocate()
+      {
+        if (content_ && toDelete_)
+        {
+          delete content_;
+          toDelete_ = false;
+          content_ = NULL;
+        }
+      }
+
+    public:
+      SmartContainer() : content_(NULL), toDelete_(false), isReadOnly_(true)
+      {
+      }
+
+      ~SmartContainer()
+      {
+        Deallocate();
+      }
+
+      void Allocate()
+      {
+        Deallocate();
+        content_ = new T;
+        toDelete_ = true;
+        isReadOnly_ = false;
+      }
+
+      void TakeOwnership(T* content)
+      {
+        if (content == NULL)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
+        Deallocate();
+        content_ = content;
+        toDelete_ = true;
+        isReadOnly_ = false;
+      }
+
+      void SetReference(T& content)   // Read and write assign, without transfering ownership
+      {
+        Deallocate();
+        content_ = &content;
+        toDelete_ = false;
+        isReadOnly_ = false;
+      }
+
+      void SetConstReference(const T& content)   // Read-only assign, without transfering ownership
+      {
+        Deallocate();
+        content_ = &const_cast<T&>(content);
+        toDelete_ = false;
+        isReadOnly_ = true;
+      }
+
+      bool HasContent() const
+      {
+        return content_ != NULL;
+      }
+
+      T& GetContent()
+      {
+        if (content_ == NULL)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+
+        if (isReadOnly_)
+        {
+          throw OrthancException(ErrorCode_ReadOnly);
+        }
+
+        return *content_;
+      }
+
+      const T& GetConstContent() const
+      {
+        if (content_ == NULL)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+
+        return *content_;
+      }
+    };
+
+
+    SmartContainer<std::string>  buffer_;
+    SmartContainer<ParsedDicomFile>  parsed_;
+    SmartContainer<DicomMap>  summary_;
+    SmartContainer<Json::Value>  json_;
+
+    std::string remoteAet_;
+    std::string calledAet_;
+    ServerIndex::MetadataMap metadata_;
+
+    void ComputeMissingInformation();
+
+  public:
+    void SetBuffer(const std::string& dicom)
+    {
+      buffer_.SetConstReference(dicom);
+    }
+
+    void SetParsedDicomFile(ParsedDicomFile& parsed)
+    {
+      parsed_.SetReference(parsed);
+    }
+
+    void SetSummary(const DicomMap& summary)
+    {
+      summary_.SetConstReference(summary);
+    }
+
+    void SetJson(const Json::Value& json)
+    {
+      json_.SetConstReference(json);
+    }
+
+    const std::string& GetRemoteAet() const
+    {
+      return remoteAet_;
+    }
+
+    void SetRemoteAet(const std::string& aet)
+    {
+      remoteAet_ = aet;
+    }
+
+    const std::string& GetCalledAet() const
+    {
+      return calledAet_;
+    }
+
+    void SetCalledAet(const std::string& aet)
+    {
+      calledAet_ = aet;
+    }
+
+    void AddMetadata(ResourceType level,
+                     MetadataType metadata,
+                     const std::string& value);
+
+    const ServerIndex::MetadataMap& GetMetadata() const
+    {
+      return metadata_;
+    }
+
+    ServerIndex::MetadataMap& GetMetadata()
+    {
+      return metadata_;
+    }
+
+    const char* GetBufferData();
+
+    size_t GetBufferSize();
+
+    const DicomMap& GetSummary();
+    
+    const Json::Value& GetJson();
+  };
+}
--- a/OrthancServer/DicomModification.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomModification.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -39,8 +39,23 @@
 #include <memory>   // For std::auto_ptr
 #include <glog/logging.h>
 
+
+static const std::string ORTHANC_DEIDENTIFICATION_METHOD = "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1";
+
 namespace Orthanc
 {
+  void DicomModification::MarkNotOrthancAnonymization()
+  {
+    Replacements::iterator it = replacements_.find(DICOM_TAG_DEIDENTIFICATION_METHOD);
+
+    if (it != replacements_.end() &&
+        it->second == ORTHANC_DEIDENTIFICATION_METHOD)
+    {
+      replacements_.erase(it);
+    }
+  }
+
+
   void DicomModification::MapDicomIdentifier(ParsedDicomFile& dicom,
                                              ResourceType level)
   {
@@ -90,18 +105,29 @@
   {
     removePrivateTags_ = false;
     level_ = ResourceType_Instance;
+    allowManualIdentifiers_ = true;
   }
 
   void DicomModification::Keep(const DicomTag& tag)
   {
     removals_.erase(tag);
     replacements_.erase(tag);
+
+    if (FromDcmtkBridge::IsPrivateTag(tag))
+    {
+      privateTagsToKeep_.insert(tag);
+    }
+
+    MarkNotOrthancAnonymization();
   }
 
   void DicomModification::Remove(const DicomTag& tag)
   {
     removals_.insert(tag);
     replacements_.erase(tag);
+    privateTagsToKeep_.erase(tag);
+
+    MarkNotOrthancAnonymization();
   }
 
   bool DicomModification::IsRemoved(const DicomTag& tag) const
@@ -110,10 +136,17 @@
   }
 
   void DicomModification::Replace(const DicomTag& tag,
-                                  const std::string& value)
+                                  const std::string& value,
+                                  bool safeForAnonymization)
   {
     removals_.erase(tag);
+    privateTagsToKeep_.erase(tag);
     replacements_[tag] = value;
+
+    if (!safeForAnonymization)
+    {
+      MarkNotOrthancAnonymization();
+    }
   }
 
   bool DicomModification::IsReplaced(const DicomTag& tag) const
@@ -138,12 +171,22 @@
   void DicomModification::SetRemovePrivateTags(bool removed)
   {
     removePrivateTags_ = removed;
+
+    if (!removed)
+    {
+      MarkNotOrthancAnonymization();
+    }
   }
 
   void DicomModification::SetLevel(ResourceType level)
   {
     uidMap_.clear();
     level_ = level;
+
+    if (level != ResourceType_Patient)
+    {
+      MarkNotOrthancAnonymization();
+    }
   }
 
   void DicomModification::SetupAnonymization()
@@ -153,6 +196,7 @@
     removePrivateTags_ = true;
     level_ = ResourceType_Patient;
     uidMap_.clear();
+    privateTagsToKeep_.clear();
 
     // This is Table E.1-1 from PS 3.15-2008 - DICOM Part 15: Security and System Management Profiles
     removals_.insert(DicomTag(0x0008, 0x0014));  // Instance Creator UID
@@ -211,7 +255,7 @@
     removals_.insert(DicomTag(0x0010, 0x2000));  // Medical Alerts
 
     // Set the DeidentificationMethod tag
-    replacements_.insert(std::make_pair(DicomTag(0x0012, 0x0063), "Orthanc " ORTHANC_VERSION " - PS 3.15-2008 Table E.1-1"));
+    replacements_.insert(std::make_pair(DICOM_TAG_DEIDENTIFICATION_METHOD, ORTHANC_DEIDENTIFICATION_METHOD));
 
     // Set the PatientIdentityRemoved tag
     replacements_.insert(std::make_pair(DicomTag(0x0012, 0x0062), "YES"));
@@ -236,36 +280,111 @@
     {
       throw OrthancException(ErrorCode_BadRequest);
     }
+    
 
+    // 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);
     }
 
-    if (level_ > ResourceType_Patient && IsReplaced(DICOM_TAG_PATIENT_ID))
+    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);
+      }
+
+      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);
+      }
+
+      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);
+      }
+    }
+
+
+    // 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);
     }
 
-    if (level_ > ResourceType_Study && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    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);
+      }
+
+      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);
+      }
+    }
+
+
+    // 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);
+    }
+
+    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);
     }
 
-    if (level_ > ResourceType_Series && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    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);
+      }
+    }
+
+
+    // 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);
     }
 
+    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);
+    }
+
+    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);
+    }
+
+
     // (1) Remove the private tags, if need be
     if (removePrivateTags_)
     {
-      toModify.RemovePrivateTags();
+      toModify.RemovePrivateTags(privateTagsToKeep_);
     }
 
     // (2) Remove the tags specified by the user
-    for (Removals::const_iterator it = removals_.begin(); 
+    for (SetOfTags::const_iterator it = removals_.begin(); 
          it != removals_.end(); ++it)
     {
       toModify.Remove(*it);
@@ -279,17 +398,20 @@
     }
 
     // (4) Update the DICOM identifiers
-    if (level_ <= ResourceType_Study)
+    if (level_ <= ResourceType_Study &&
+        !IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
     {
       MapDicomIdentifier(toModify, ResourceType_Study);
     }
 
-    if (level_ <= ResourceType_Series)
+    if (level_ <= ResourceType_Series &&
+        !IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
     {
       MapDicomIdentifier(toModify, ResourceType_Series);
     }
 
-    if (level_ <= ResourceType_Instance)  // Always true
+    if (level_ <= ResourceType_Instance &&  // Always true
+        !IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
     {
       MapDicomIdentifier(toModify, ResourceType_Instance);
     }
--- a/OrthancServer/DicomModification.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomModification.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -46,19 +46,23 @@
      **/
 
   private:
-    typedef std::set<DicomTag> Removals;
+    typedef std::set<DicomTag> SetOfTags;
     typedef std::map<DicomTag, std::string> Replacements;
     typedef std::map< std::pair<ResourceType, std::string>, std::string>  UidMap;
 
-    Removals removals_;
+    SetOfTags removals_;
     Replacements replacements_;
     bool removePrivateTags_;
     ResourceType level_;
     UidMap uidMap_;
+    SetOfTags privateTagsToKeep_;
+    bool allowManualIdentifiers_;
 
     void MapDicomIdentifier(ParsedDicomFile& dicom,
                             ResourceType level);
 
+    void MarkNotOrthancAnonymization();
+
   public:
     DicomModification();
 
@@ -69,7 +73,8 @@
     bool IsRemoved(const DicomTag& tag) const;
 
     void Replace(const DicomTag& tag,
-                 const std::string& value);
+                 const std::string& value,
+                 bool safeForAnonymization = false);
 
     bool IsReplaced(const DicomTag& tag) const;
 
@@ -92,5 +97,15 @@
     void SetupAnonymization();
 
     void Apply(ParsedDicomFile& toModify);
+
+    void SetAllowManualIdentifiers(bool check)
+    {
+      allowManualIdentifiers_ = check;
+    }
+
+    bool AreAllowManualIdentifiers() const
+    {
+      return allowManualIdentifiers_;
+    }
   };
 }
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/DicomProtocol/DicomServer.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/DicomServer.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -190,6 +190,11 @@
 
     LOG(INFO) << "DICOM server stopping";
 
+    if (server->isThreaded_)
+    {
+      server->bagOfDispatchers_.StopAll();
+    }
+
     /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */
     /* is the counterpart of ASC_initializeNetwork(...) which was called above. */
     cond = ASC_dropNetwork(&net);
@@ -273,11 +278,20 @@
       throw OrthancException("Too short AET");
     }
 
+    if (aet.size() > 16)
+    {
+      throw OrthancException("AET must be shorter than 16 characters");
+    }
+
     for (size_t i = 0; i < aet.size(); i++)
     {
-      if (!isalnum(aet[i]) && aet[i] != '-')
+      if (!(aet[i] == '-' ||
+            aet[i] == '_' ||
+            isdigit(aet[i]) ||
+            (aet[i] >= 'A' && aet[i] <= 'Z')))
       {
-        throw OrthancException("Only alphanumeric characters are allowed in AET");
+        LOG(WARNING) << "For best interoperability, only upper case, alphanumeric characters should be present in AET: \"" << aet << "\"";
+        break;
       }
     }
 
@@ -403,8 +417,6 @@
     {
       pimpl_->thread_.join();
     }
-
-    bagOfDispatchers_.StopAll();
   }
 
   bool DicomServer::IsMyAETitle(const std::string& aet) const
--- a/OrthancServer/DicomProtocol/DicomServer.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/DicomServer.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -135,6 +135,8 @@
   struct DicomUserConnection::PImpl
   {
     // Connection state
+    uint32_t dimseTimeout_;
+    uint32_t acseTimeout_;
     T_ASC_Network* net_;
     T_ASC_Parameters* params_;
     T_ASC_Association* assoc_;
@@ -154,7 +156,8 @@
   {
     if (cond.bad())
     {
-      throw OrthancException("DicomUserConnection: " + std::string(cond.text()));
+      LOG(ERROR) << "DicomUserConnection: " << std::string(cond.text());
+       throw OrthancException(ErrorCode_NetworkProtocol);
     }
   }
 
@@ -162,7 +165,8 @@
   {
     if (!IsOpen())
     {
-      throw OrthancException("DicomUserConnection: First open the connection");
+      LOG(ERROR) << "DicomUserConnection: First open the connection";
+      throw OrthancException(ErrorCode_NetworkProtocol);
     }
   }
 
@@ -323,7 +327,7 @@
     DcmDataset* statusDetail = NULL;
     Check(DIMSE_storeUser(assoc_, presID, &req,
                           NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL,
-                          /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0,
+                          /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_,
                           &rsp, &statusDetail, NULL));
 
     if (statusDetail != NULL) 
@@ -450,7 +454,7 @@
     int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass);
     if (presID == 0)
     {
-      throw OrthancException("DicomUserConnection: The C-FIND command is not supported by the distant AET");
+      throw OrthancException("DicomUserConnection: The C-FIND command is not supported by the remote AET");
     }
 
     T_DIMSE_C_FindRQ request;
@@ -464,7 +468,8 @@
     DcmDataset* statusDetail = NULL;
     OFCondition cond = DIMSE_findUser(pimpl_->assoc_, presID, &request, dataset.get(),
                                       FindCallback, &result,
-                                      /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0,
+                                      /*opt_blockMode*/ DIMSE_BLOCKING, 
+                                      /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
                                       &response, &statusDetail);
 
     if (statusDetail)
@@ -541,7 +546,7 @@
     int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass);
     if (presID == 0)
     {
-      throw OrthancException("DicomUserConnection: The C-MOVE command is not supported by the distant AET");
+      throw OrthancException("DicomUserConnection: The C-MOVE command is not supported by the remote AET");
     }
 
     T_DIMSE_C_MoveRQ request;
@@ -557,7 +562,8 @@
     DcmDataset* responseIdentifiers = NULL;
     OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset.get(),
                                       NULL, NULL,
-                                      /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0,
+                                      /*opt_blockMode*/ DIMSE_BLOCKING, 
+                                      /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
                                       pimpl_->net_, NULL, NULL,
                                       &response, &statusDetail, &responseIdentifiers);
 
@@ -608,12 +614,13 @@
     pimpl_(new PImpl),
     preferredTransferSyntax_(DEFAULT_PREFERRED_TRANSFER_SYNTAX),
     localAet_("STORESCU"),
-    distantAet_("ANY-SCP"),
-    distantHost_("127.0.0.1")
+    remoteAet_("ANY-SCP"),
+    remoteHost_("127.0.0.1")
   {
-    distantPort_ = 104;
+    remotePort_ = 104;
     manufacturer_ = ModalityManufacturer_Generic;
 
+    SetTimeout(10); 
     pimpl_->net_ = NULL;
     pimpl_->params_ = NULL;
     pimpl_->assoc_ = NULL;
@@ -635,10 +642,10 @@
 
   void DicomUserConnection::Connect(const RemoteModalityParameters& parameters)
   {
-    SetDistantApplicationEntityTitle(parameters.GetApplicationEntityTitle());
-    SetDistantHost(parameters.GetHost());
-    SetDistantPort(parameters.GetPort());
-    SetDistantManufacturer(parameters.GetManufacturer());
+    SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle());
+    SetRemoteHost(parameters.GetHost());
+    SetRemotePort(parameters.GetPort());
+    SetRemoteManufacturer(parameters.GetManufacturer());
   }
 
 
@@ -651,16 +658,16 @@
     }
   }
 
-  void DicomUserConnection::SetDistantApplicationEntityTitle(const std::string& aet)
+  void DicomUserConnection::SetRemoteApplicationEntityTitle(const std::string& aet)
   {
-    if (distantAet_ != aet)
+    if (remoteAet_ != aet)
     {
       Close();
-      distantAet_ = aet;
+      remoteAet_ = aet;
     }
   }
 
-  void DicomUserConnection::SetDistantManufacturer(ModalityManufacturer manufacturer)
+  void DicomUserConnection::SetRemoteManufacturer(ModalityManufacturer manufacturer)
   {
     if (manufacturer_ != manufacturer)
     {
@@ -684,26 +691,26 @@
   }
 
 
-  void DicomUserConnection::SetDistantHost(const std::string& host)
+  void DicomUserConnection::SetRemoteHost(const std::string& host)
   {
-    if (distantHost_ != host)
+    if (remoteHost_ != host)
     {
       if (host.size() > HOST_NAME_MAX - 10)
       {
-        throw OrthancException("Distant host name is too long");
+        throw OrthancException("Remote host name is too long");
       }
 
       Close();
-      distantHost_ = host;
+      remoteHost_ = host;
     }
   }
 
-  void DicomUserConnection::SetDistantPort(uint16_t port)
+  void DicomUserConnection::SetRemotePort(uint16_t port)
   {
-    if (distantPort_ != port)
+    if (remotePort_ != port)
     {
       Close();
-      distantPort_ = port;
+      remotePort_ = port;
     }
   }
 
@@ -716,30 +723,30 @@
     }
 
     LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << GetLocalApplicationEntityTitle() 
-              << "\" to AET \"" << GetDistantApplicationEntityTitle() << "\" on host "
-              << GetDistantHost() << ":" << GetDistantPort() 
-              << " (manufacturer: " << EnumerationToString(GetDistantManufacturer()) << ")";
+              << "\" to AET \"" << GetRemoteApplicationEntityTitle() << "\" on host "
+              << GetRemoteHost() << ":" << GetRemotePort() 
+              << " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")";
 
-    Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ 30, &pimpl_->net_));
+    Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_));
     Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
 
     // Set this application's title and the called application's title in the params
-    Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), distantAet_.c_str(), NULL));
+    Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL));
 
-    // Set the network addresses of the local and distant entities
+    // Set the network addresses of the local and remote entities
     char localHost[HOST_NAME_MAX];
     gethostname(localHost, HOST_NAME_MAX - 1);
 
-    char distantHostAndPort[HOST_NAME_MAX];
+    char remoteHostAndPort[HOST_NAME_MAX];
 
 #ifdef _MSC_VER
     _snprintf
 #else
       snprintf
 #endif
-      (distantHostAndPort, HOST_NAME_MAX - 1, "%s:%d", distantHost_.c_str(), distantPort_);
+      (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", remoteHost_.c_str(), remotePort_);
 
-    Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, distantHostAndPort));
+    Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort));
 
     // Set various options
     Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false));
@@ -816,7 +823,8 @@
     CheckIsOpen();
     DIC_US status;
     Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, 
-                         /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ 0,
+                         /*opt_blockMode*/ DIMSE_BLOCKING, 
+                         /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
                          &status, NULL));
     return status == STATUS_Success;
   }
@@ -863,9 +871,29 @@
     Move(targetAet, map);
   }
 
-  void DicomUserConnection::SetConnectionTimeout(uint32_t seconds)
+
+  void DicomUserConnection::SetTimeout(uint32_t seconds)
   {
+    if (seconds <= 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
     dcmConnectionTimeout.set(seconds);
+    pimpl_->dimseTimeout_ = seconds;
+    pimpl_->acseTimeout_ = 10;
+  }
+
+
+  void DicomUserConnection::DisableTimeout()
+  {
+    /**
+     * Global timeout (seconds) for connecting to remote hosts.
+     * Default value is -1 which selects infinite timeout, i.e. blocking connect().
+     */
+    dcmConnectionTimeout.set(-1);
+    pimpl_->dimseTimeout_ = 0;
+    pimpl_->acseTimeout_ = 10;
   }
 
 
@@ -914,7 +942,7 @@
              defaultStorageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES)
     {
       // Make room in the default storage syntaxes
-      assert(defaultStorageSOPClasses_.size() > 0);  // Necessarily true because condition (*) is false
+      assert(!defaultStorageSOPClasses_.empty());  // Necessarily true because condition (*) is false
       defaultStorageSOPClasses_.erase(*defaultStorageSOPClasses_.rbegin());
     }
 
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -60,9 +60,9 @@
     // Connection parameters
     std::string preferredTransferSyntax_;
     std::string localAet_;
-    std::string distantAet_;
-    std::string distantHost_;
-    uint16_t distantPort_;
+    std::string remoteAet_;
+    std::string remoteHost_;
+    uint16_t remotePort_;
     ModalityManufacturer manufacturer_;
     std::set<std::string> storageSOPClasses_;
     std::list<std::string> reservedStorageSOPClasses_;
@@ -97,30 +97,30 @@
       return localAet_;
     }
 
-    void SetDistantApplicationEntityTitle(const std::string& aet);
+    void SetRemoteApplicationEntityTitle(const std::string& aet);
 
-    const std::string& GetDistantApplicationEntityTitle() const
+    const std::string& GetRemoteApplicationEntityTitle() const
     {
-      return distantAet_;
+      return remoteAet_;
     }
 
-    void SetDistantHost(const std::string& host);
+    void SetRemoteHost(const std::string& host);
 
-    const std::string& GetDistantHost() const
+    const std::string& GetRemoteHost() const
     {
-      return distantHost_;
+      return remoteHost_;
     }
 
-    void SetDistantPort(uint16_t port);
+    void SetRemotePort(uint16_t port);
 
-    uint16_t GetDistantPort() const
+    uint16_t GetRemotePort() const
     {
-      return distantPort_;
+      return remotePort_;
     }
 
-    void SetDistantManufacturer(ModalityManufacturer manufacturer);
+    void SetRemoteManufacturer(ModalityManufacturer manufacturer);
 
-    ModalityManufacturer GetDistantManufacturer() const
+    ModalityManufacturer GetRemoteManufacturer() const
     {
       return manufacturer_;
     }
@@ -177,6 +177,8 @@
                       const std::string& seriesUid,
                       const std::string& instanceUid);
 
-    static void SetConnectionTimeout(uint32_t seconds);
+    void SetTimeout(uint32_t seconds);
+
+    void DisableTimeout();
   };
 }
--- a/OrthancServer/DicomProtocol/IApplicationEntityFilter.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/IApplicationEntityFilter.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -51,5 +51,9 @@
     virtual bool IsAllowedRequest(const std::string& callingIp,
                                   const std::string& callingAet,
                                   DicomRequestType type) = 0;
+
+    virtual bool IsAllowedTransferSyntax(const std::string& callingIp,
+                                         const std::string& callingAet,
+                                         TransferSyntax syntax) = 0;
   };
 }
--- a/OrthancServer/DicomProtocol/IFindRequestHandler.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/IFindRequestHandler.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/DicomProtocol/IMoveRequestHandler.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/IMoveRequestHandler.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/DicomProtocol/IStoreRequestHandler.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/IStoreRequestHandler.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -50,6 +50,7 @@
     virtual void Handle(const std::string& dicomFile,
                         const DicomMap& dicomSummary,
                         const Json::Value& dicomJson,
-                        const std::string& distantAet) = 0;
+                        const std::string& remoteAet,
+                        const std::string& calledAet) = 0;
   };
 }
--- a/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/RemoteModalityParameters.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/DicomProtocol/RemoteModalityParameters.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/RemoteModalityParameters.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/ReusableDicomUserConnection.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -50,10 +50,10 @@
                                          ModalityManufacturer manufacturer)
   {
     if (connection_ != NULL &&
-        connection_->GetDistantApplicationEntityTitle() == remoteAet &&
-        connection_->GetDistantHost() == address &&
-        connection_->GetDistantPort() == port &&
-        connection_->GetDistantManufacturer() == manufacturer)
+        connection_->GetRemoteApplicationEntityTitle() == remoteAet &&
+        connection_->GetRemoteHost() == address &&
+        connection_->GetRemotePort() == port &&
+        connection_->GetRemoteManufacturer() == manufacturer)
     {
       // The current connection can be reused
       LOG(INFO) << "Reusing the previous SCU connection";
@@ -64,10 +64,10 @@
 
     connection_ = new DicomUserConnection();
     connection_->SetLocalApplicationEntityTitle(localAet_);
-    connection_->SetDistantApplicationEntityTitle(remoteAet);
-    connection_->SetDistantHost(address);
-    connection_->SetDistantPort(port);
-    connection_->SetDistantManufacturer(manufacturer);
+    connection_->SetRemoteApplicationEntityTitle(remoteAet);
+    connection_->SetRemoteHost(address);
+    connection_->SetRemotePort(port);
+    connection_->SetRemoteManufacturer(manufacturer);
     connection_->Open();
   }
     
@@ -152,9 +152,15 @@
     Close();
   }
 
-  void ReusableDicomUserConnection::SetMillisecondsBeforeClose(unsigned int ms)
+  void ReusableDicomUserConnection::SetMillisecondsBeforeClose(uint64_t ms)
   {
     boost::mutex::scoped_lock lock(mutex_);
+
+    if (ms == 0)
+    {
+      ms = 1;
+    }
+
     timeBeforeClose_ = boost::posix_time::milliseconds(ms);
   }
 
@@ -172,6 +178,14 @@
 
   void ReusableDicomUserConnection::Unlock()
   {
+    if (connection_ != NULL &&
+        connection_->GetRemoteManufacturer() == ModalityManufacturer_StoreScp)
+    {
+      // "storescp" from DCMTK has problems when reusing a
+      // connection. Always close.
+      Close();
+    }
+
     lastUse_ = Now();
     mutex_.unlock();
   }
--- a/OrthancServer/DicomProtocol/ReusableDicomUserConnection.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/DicomProtocol/ReusableDicomUserConnection.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -88,12 +88,12 @@
 
     virtual ~ReusableDicomUserConnection();
 
-    unsigned int GetMillisecondsBeforeClose() const
+    uint64_t GetMillisecondsBeforeClose() const
     {
-      return static_cast<unsigned int>(timeBeforeClose_.total_milliseconds());
+      return static_cast<uint64_t>(timeBeforeClose_.total_milliseconds());
     }
 
-    void SetMillisecondsBeforeClose(unsigned int ms);
+    void SetMillisecondsBeforeClose(uint64_t ms);
 
     const std::string& GetLocalApplicationEntityTitle() const;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ExportedResource.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,69 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ExportedResource.h"
+
+#include "../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  void ExportedResource::Format(Json::Value& item) const
+  {
+    item = Json::objectValue;
+    item["Seq"] = static_cast<int>(seq_);
+    item["ResourceType"] = EnumerationToString(resourceType_);
+    item["ID"] = publicId_;
+    item["Path"] = GetBasePath(resourceType_, publicId_);
+    item["RemoteModality"] = modality_;
+    item["Date"] = date_;
+
+    // WARNING: Do not add "break" below and do not reorder the case items!
+    switch (resourceType_)
+    {
+      case ResourceType_Instance:
+        item["SOPInstanceUID"] = sopInstanceUid_;
+
+      case ResourceType_Series:
+        item["SeriesInstanceUID"] = seriesInstanceUid_;
+
+      case ResourceType_Study:
+        item["StudyInstanceUID"] = studyInstanceUid_;
+
+      case ResourceType_Patient:
+        item["PatientID"] = patientId_;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ExportedResource.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,125 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ServerEnumerations.h"
+#include "../Core/Toolbox.h"
+
+#include <string>
+#include <json/value.h>
+
+namespace Orthanc
+{
+  class ExportedResource
+  {
+  private:
+    int64_t      seq_;
+    ResourceType resourceType_;
+    std::string  publicId_;
+    std::string  modality_;
+    std::string  date_;
+    std::string  patientId_;
+    std::string  studyInstanceUid_;
+    std::string  seriesInstanceUid_;
+    std::string  sopInstanceUid_;
+
+  public:
+    ExportedResource(int64_t seq,
+                     ResourceType resourceType,
+                     const std::string& publicId,
+                     const std::string& modality,
+                     const std::string& date,
+                     const std::string& patientId,
+                     const std::string& studyInstanceUid,
+                     const std::string& seriesInstanceUid,
+                     const std::string& sopInstanceUid) :
+      seq_(seq),
+      resourceType_(resourceType),
+      publicId_(publicId),
+      modality_(modality),
+      date_(date),
+      patientId_(patientId),
+      studyInstanceUid_(studyInstanceUid),
+      seriesInstanceUid_(seriesInstanceUid),
+      sopInstanceUid_(sopInstanceUid)
+    {
+    }
+
+    int64_t  GetSeq() const
+    {
+      return seq_;
+    }
+
+    ResourceType  GetResourceType() const
+    {
+      return resourceType_;
+    }
+
+    const std::string&  GetPublicId() const
+    {
+      return publicId_;
+    }
+
+    const std::string& GetModality() const
+    {
+      return modality_;
+    }
+
+    const std::string& GetDate() const
+    {
+      return date_;
+    }
+
+    const std::string& GetPatientId() const
+    {
+      return patientId_;
+    }
+
+    const std::string& GetStudyInstanceUid() const
+    {
+      return studyInstanceUid_;
+    }
+
+    const std::string& GetSeriesInstanceUid() const
+    {
+      return seriesInstanceUid_;
+    }
+
+    const std::string& GetSopInstanceUid() const
+    {
+      return sopInstanceUid_;
+    }
+
+    void Format(Json::Value& item) const;
+  };
+}
--- a/OrthancServer/FromDcmtkBridge.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/FromDcmtkBridge.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -41,6 +41,7 @@
 
 #include "FromDcmtkBridge.h"
 #include "ToDcmtkBridge.h"
+#include "OrthancInitialization.h"
 #include "../Core/Toolbox.h"
 #include "../Core/OrthancException.h"
 #include "../Core/ImageFormats/PngWriter.h"
@@ -61,6 +62,7 @@
 #include <dcmtk/dcmdata/dcistrmb.h>
 #include <dcmtk/dcmdata/dcuid.h>
 #include <dcmtk/dcmdata/dcmetinf.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
 
 #include <dcmtk/dcmdata/dcvrae.h>
 #include <dcmtk/dcmdata/dcvras.h>
@@ -86,6 +88,7 @@
 #include <dcmtk/dcmdata/dcpixel.h>
 #include <dcmtk/dcmdata/dcpixseq.h>
 #include <dcmtk/dcmdata/dcpxitem.h>
+#include <dcmtk/dcmdata/dcvrat.h>
 
 
 #include <boost/math/special_functions/round.hpp>
@@ -115,8 +118,46 @@
             GetCharValue(c[3]));
   }
 
+
+  Encoding FromDcmtkBridge::DetectEncoding(DcmDataset& dataset)
+  {
+    // By default, Latin1 encoding is assumed
+    std::string s = Configuration::GetGlobalStringParameter("DefaultEncoding", "");
+    Encoding encoding = s.empty() ? Encoding_Latin1 : StringToEncoding(s.c_str());
+
+    OFString tmp;
+    if (dataset.findAndGetOFString(DCM_SpecificCharacterSet, tmp).good())
+    {
+      std::string characterSet = Toolbox::StripSpaces(std::string(tmp.c_str()));
+
+      if (characterSet.empty())
+      {
+        // Empty specific character set tag: Use the default encoding
+      }
+      else if (GetDicomEncoding(encoding, characterSet.c_str()))
+      {
+        // The specific character set is supported by the Orthanc core
+      }
+      else
+      {
+        LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet
+                     << ", fallback to ASCII (remove all special characters)";
+        encoding = Encoding_Ascii;
+      }
+    }
+    else
+    {
+      // No specific character set tag: Use the default encoding
+    }
+
+    return encoding;
+  }
+
+
   void FromDcmtkBridge::Convert(DicomMap& target, DcmDataset& dataset)
   {
+    Encoding encoding = DetectEncoding(dataset);
+
     target.Clear();
     for (unsigned long i = 0; i < dataset.card(); i++)
     {
@@ -125,19 +166,52 @@
       {
         target.SetValue(element->getTag().getGTag(),
                         element->getTag().getETag(),
-                        ConvertLeafElement(*element));
+                        ConvertLeafElement(*element, encoding));
       }
     }
   }
 
 
+  DicomTag FromDcmtkBridge::Convert(const DcmTag& tag)
+  {
+    return DicomTag(tag.getGTag(), tag.getETag());
+  }
+
+
   DicomTag FromDcmtkBridge::GetTag(const DcmElement& element)
   {
     return DicomTag(element.getGTag(), element.getETag());
   }
 
 
-  DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element)
+  bool FromDcmtkBridge::IsPrivateTag(DcmTag& tag)
+  {
+#if 1
+    DcmTagKey tmp(tag.getGTag(), tag.getETag());
+    return tmp.isPrivate();
+#else
+    // Implementation for Orthanc versions <= 0.8.5
+    return (tag.getPrivateCreator() != NULL ||
+            !strcmp("PrivateCreator", tag.getTagName()));  // TODO - This may change with future versions of DCMTK
+#endif
+  }
+
+
+  bool FromDcmtkBridge::IsPrivateTag(const DicomTag& tag)
+  {
+#if 1
+    DcmTagKey tmp(tag.GetGroup(), tag.GetElement());
+    return tmp.isPrivate();
+#else
+    // Implementation for Orthanc versions <= 0.8.5
+    DcmTag tmp(tag.GetGroup(), tag.GetElement());
+    return IsPrivateTag(tmp);
+#endif
+  }
+
+
+  DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element,
+                                                  Encoding encoding)
   {
     if (!element.isLeaf())
     {
@@ -151,7 +225,7 @@
           c != NULL)
       {
         std::string s(c);
-        std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1); // TODO Parameter?
+        std::string utf8 = Toolbox::ConvertToUtf8(s, encoding);
         return new DicomString(utf8);
       }
       else
@@ -173,9 +247,8 @@
         case EVR_OB:  // other byte
         case EVR_OF:  // other float
         case EVR_OW:  // other word
-        case EVR_AT:  // attribute tag
         case EVR_UN:  // unknown value representation
-          return new DicomNullValue();
+          return new DicomNullValue;
     
           /**
            * String types, should never happen at this point because of
@@ -197,11 +270,11 @@
         case EVR_UT:  // unlimited text
         case EVR_PN:  // person name
         case EVR_UI:  // unique identifier
-          return new DicomNullValue();
+          return new DicomNullValue;
 
 
           /**
-           * Numerical types
+           * Numberic types
            **/ 
       
         case EVR_SL:  // signed long
@@ -210,7 +283,7 @@
           if (dynamic_cast<DcmSignedLong&>(element).getSint32(f).good())
             return new DicomString(boost::lexical_cast<std::string>(f));
           else
-            return new DicomNullValue();
+            return new DicomNullValue;
         }
 
         case EVR_SS:  // signed short
@@ -219,7 +292,7 @@
           if (dynamic_cast<DcmSignedShort&>(element).getSint16(f).good())
             return new DicomString(boost::lexical_cast<std::string>(f));
           else
-            return new DicomNullValue();
+            return new DicomNullValue;
         }
 
         case EVR_UL:  // unsigned long
@@ -228,7 +301,7 @@
           if (dynamic_cast<DcmUnsignedLong&>(element).getUint32(f).good())
             return new DicomString(boost::lexical_cast<std::string>(f));
           else
-            return new DicomNullValue();
+            return new DicomNullValue;
         }
 
         case EVR_US:  // unsigned short
@@ -237,7 +310,7 @@
           if (dynamic_cast<DcmUnsignedShort&>(element).getUint16(f).good())
             return new DicomString(boost::lexical_cast<std::string>(f));
           else
-            return new DicomNullValue();
+            return new DicomNullValue;
         }
 
         case EVR_FL:  // float single-precision
@@ -246,7 +319,7 @@
           if (dynamic_cast<DcmFloatingPointSingle&>(element).getFloat32(f).good())
             return new DicomString(boost::lexical_cast<std::string>(f));
           else
-            return new DicomNullValue();
+            return new DicomNullValue;
         }
 
         case EVR_FD:  // float double-precision
@@ -255,7 +328,26 @@
           if (dynamic_cast<DcmFloatingPointDouble&>(element).getFloat64(f).good())
             return new DicomString(boost::lexical_cast<std::string>(f));
           else
-            return new DicomNullValue();
+            return new DicomNullValue;
+        }
+
+
+        /**
+         * Attribute tag.
+         **/
+
+        case EVR_AT:
+        {
+          DcmTagKey tag;
+          if (dynamic_cast<DcmAttributeTag&>(element).getTagVal(tag, 0).good())
+          {
+            DicomTag t(tag.getGroup(), tag.getElement());
+            return new DicomString(t.Format());
+          }
+          else
+          {
+            return new DicomNullValue;
+          }
         }
 
 
@@ -313,25 +405,28 @@
 
   static void StoreElement(Json::Value& target,
                            DcmElement& element,
-                           unsigned int maxStringLength);
+                           unsigned int maxStringLength,
+                           Encoding encoding);
 
   static void StoreItem(Json::Value& target,
                         DcmItem& item,
-                        unsigned int maxStringLength)
+                        unsigned int maxStringLength,
+                        Encoding encoding)
   {
     target = Json::Value(Json::objectValue);
 
     for (unsigned long i = 0; i < item.card(); i++)
     {
       DcmElement* element = item.getElement(i);
-      StoreElement(target, *element, maxStringLength);
+      StoreElement(target, *element, maxStringLength, encoding);
     }
   }
 
 
   static void StoreElement(Json::Value& target,
                            DcmElement& element,
-                           unsigned int maxStringLength)
+                           unsigned int maxStringLength,
+                           Encoding encoding)
   {
     assert(target.type() == Json::objectValue);
 
@@ -356,7 +451,7 @@
         value["PrivateCreator"] = tagbis.getPrivateCreator();
       }
 
-      std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(element));
+      std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(element, encoding));
       if (v->IsNull())
       {
         value["Type"] = "Null";
@@ -393,7 +488,7 @@
       {
         DcmItem* child = sequence.getItem(i);
         Json::Value& v = children.append(Json::objectValue);
-        StoreItem(v, *child, maxStringLength);
+        StoreItem(v, *child, maxStringLength, encoding);
       }  
 
       target[formattedTag]["Name"] = tagName;
@@ -407,7 +502,7 @@
                                DcmDataset& dataset,
                                unsigned int maxStringLength)
   {
-    StoreItem(root, dataset, maxStringLength);
+    StoreItem(root, dataset, maxStringLength, DetectEncoding(dataset));
   }
 
 
@@ -475,7 +570,7 @@
         isxdigit(name[1]) &&
         isxdigit(name[2]) &&
         isxdigit(name[3]) &&
-        name[4] == '-' &&
+        (name[4] == '-' || name[4] == ',') &&
         isxdigit(name[5]) &&
         isxdigit(name[6]) &&
         isxdigit(name[7]) &&
@@ -573,7 +668,7 @@
   }
 
   bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
-                                           DcmDataset* dataSet)
+                                           DcmDataset& dataSet)
   {
     // Determine the transfer syntax which shall be used to write the
     // information to the file. We always switch to the Little Endian
@@ -588,7 +683,7 @@
      * dataset into memory. We now keep the original transfer syntax
      * (if available).
      **/
-    E_TransferSyntax xfer = dataSet->getOriginalXfer();
+    E_TransferSyntax xfer = dataSet.getOriginalXfer();
     if (xfer == EXS_Unknown)
     {
       // No information about the original transfer syntax: This is
@@ -599,8 +694,9 @@
     E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength;
 
     // Create the meta-header information
-    DcmFileFormat ff(dataSet);
+    DcmFileFormat ff(&dataSet);
     ff.validateMetaInfo(xfer);
+    ff.removeInvalidGroups();
 
     // Create a memory buffer with the proper size
     uint32_t s = ff.calcElementLength(xfer, encodingType);
@@ -625,4 +721,12 @@
       return false;
     }
   }
+
+
+  bool FromDcmtkBridge::IsPNValueRepresentation(const DicomTag& tag)
+  {
+    DcmTag t(tag.GetGroup(), tag.GetElement());
+    return t.getEVR() == EVR_PN;
+  }
+
 }
--- a/OrthancServer/FromDcmtkBridge.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/FromDcmtkBridge.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -44,11 +44,20 @@
   class FromDcmtkBridge
   {
   public:
+    static Encoding DetectEncoding(DcmDataset& dataset);
+
     static void Convert(DicomMap& target, DcmDataset& dataset);
 
+    static DicomTag Convert(const DcmTag& tag);
+
     static DicomTag GetTag(const DcmElement& element);
 
-    static DicomValue* ConvertLeafElement(DcmElement& element);
+    static bool IsPrivateTag(DcmTag& tag);
+
+    static bool IsPrivateTag(const DicomTag& tag);
+
+    static DicomValue* ConvertLeafElement(DcmElement& element,
+                                          Encoding encoding);
 
     static void ToJson(Json::Value& target, 
                        DcmDataset& dataset,
@@ -95,6 +104,8 @@
     static std::string GenerateUniqueIdentifier(ResourceType level);
 
     static bool SaveToMemoryBuffer(std::string& buffer,
-                                   DcmDataset* dataSet);
+                                   DcmDataset& dataSet);
+
+    static bool IsPNValueRepresentation(const DicomTag& tag);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/IDatabaseWrapper.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,181 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Core/DicomFormat/DicomMap.h"
+#include "../Core/SQLite/ITransaction.h"
+#include "../Core/FileStorage/FileInfo.h"
+#include "IServerIndexListener.h"
+#include "ExportedResource.h"
+
+#include <list>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IDatabaseWrapper : public boost::noncopyable
+  {
+  public:
+    virtual ~IDatabaseWrapper()
+    {
+    }
+
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment) = 0;
+
+    virtual void AttachChild(int64_t parent,
+                             int64_t child) = 0;
+
+    virtual void ClearChanges() = 0;
+
+    virtual void ClearExportedResources() = 0;
+
+    virtual int64_t CreateResource(const std::string& publicId,
+                                   ResourceType type) = 0;
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment) = 0;
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type) = 0;
+
+    virtual void DeleteResource(int64_t id) = 0;
+
+    virtual void FlushToDisk() = 0;
+
+    virtual bool HasFlushToDisk() const = 0;
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id) = 0;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType) = 0;
+
+
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults) = 0;
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id) = 0;
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id) = 0;
+
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults) = 0;
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) = 0;
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id) = 0;
+
+    virtual std::string GetPublicId(int64_t resourceId) = 0;
+
+    virtual uint64_t GetResourceCount(ResourceType resourceType) = 0;
+
+    virtual ResourceType GetResourceType(int64_t resourceId) = 0;
+
+    virtual uint64_t GetTotalCompressedSize() = 0;
+    
+    virtual uint64_t GetTotalUncompressedSize() = 0;
+
+    virtual bool IsExistingResource(int64_t internalId) = 0;
+
+    virtual bool IsProtectedPatient(int64_t internalId) = 0;
+
+    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
+                                       int64_t id) = 0;
+
+    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+                                          int64_t id) = 0;
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change) = 0;
+
+    virtual void LogExportedResource(const ExportedResource& resource) = 0;
+    
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t id,
+                                  FileContentType contentType) = 0;
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property) = 0;
+
+    virtual void LookupIdentifier(std::list<int64_t>& target,
+                                  const DicomTag& tag,
+                                  const std::string& value) = 0;
+
+    virtual void LookupIdentifier(std::list<int64_t>& target,
+                                  const std::string& value) = 0;
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type) = 0;
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId) = 0;
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId) = 0;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId) = 0;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid) = 0;
+
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   const std::string& value) = 0;
+
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value) = 0;
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value) = 0;
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected) = 0;
+
+    virtual SQLite::ITransaction* StartTransaction() = 0;
+
+    virtual void SetListener(IServerIndexListener& listener) = 0;
+  };
+}
--- a/OrthancServer/IServerIndexListener.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/IServerIndexListener.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,6 +34,7 @@
 
 #include <string>
 #include "ServerEnumerations.h"
+#include "ServerIndexChange.h"
 
 namespace Orthanc
 {
@@ -48,5 +49,7 @@
                                          const std::string& publicId) = 0;
 
     virtual void SignalFileDeleted(const FileInfo& info) = 0;
+
+    virtual void SignalChange(const ServerIndexChange& change) = 0;
   };
 }
--- a/OrthancServer/Internals/CommandDispatcher.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/Internals/CommandDispatcher.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -234,10 +234,156 @@
 #endif
 
 
+
 namespace Orthanc
 {
   namespace Internals
   {
+    /**
+     * EXTRACT OF FILE "dcmdata/libsrc/dcuid.cc" FROM DCMTK 3.6.0
+     * (dcmAllStorageSOPClassUIDs).
+     *
+     * an array of const strings containing all known Storage SOP
+     * Classes that fit into the conventional
+     * PATIENT-STUDY-SERIES-INSTANCE information model,
+     * i.e. everything a Storage SCP might want to store in a PACS.
+     * Special cases such as hanging protocol storage or the Storage
+     * SOP Class are not included in this list.
+     *
+     * THIS LIST CONTAINS ALL STORAGE SOP CLASSES INCLUDING RETIRED
+     * ONES AND IS LARGER THAN 64 ENTRIES.
+     */
+
+    const char* orthancStorageSOPClassUIDs[] =
+    {
+      UID_AmbulatoryECGWaveformStorage,
+      UID_ArterialPulseWaveformStorage,
+      UID_AutorefractionMeasurementsStorage,
+      UID_BasicStructuredDisplayStorage,
+      UID_BasicTextSRStorage,
+      UID_BasicVoiceAudioWaveformStorage,
+      UID_BlendingSoftcopyPresentationStateStorage,
+      UID_BreastTomosynthesisImageStorage,
+      UID_CardiacElectrophysiologyWaveformStorage,
+      UID_ChestCADSRStorage,
+      UID_ColonCADSRStorage,
+      UID_ColorSoftcopyPresentationStateStorage,
+      UID_ComprehensiveSRStorage,
+      UID_ComputedRadiographyImageStorage,
+      UID_CTImageStorage,
+      UID_DeformableSpatialRegistrationStorage,
+      UID_DigitalIntraOralXRayImageStorageForPresentation,
+      UID_DigitalIntraOralXRayImageStorageForProcessing,
+      UID_DigitalMammographyXRayImageStorageForPresentation,
+      UID_DigitalMammographyXRayImageStorageForProcessing,
+      UID_DigitalXRayImageStorageForPresentation,
+      UID_DigitalXRayImageStorageForProcessing,
+      UID_EncapsulatedCDAStorage,
+      UID_EncapsulatedPDFStorage,
+      UID_EnhancedCTImageStorage,
+      UID_EnhancedMRColorImageStorage,
+      UID_EnhancedMRImageStorage,
+      UID_EnhancedPETImageStorage,
+      UID_EnhancedSRStorage,
+      UID_EnhancedUSVolumeStorage,
+      UID_EnhancedXAImageStorage,
+      UID_EnhancedXRFImageStorage,
+      UID_GeneralAudioWaveformStorage,
+      UID_GeneralECGWaveformStorage,
+      UID_GenericImplantTemplateStorage,
+      UID_GrayscaleSoftcopyPresentationStateStorage,
+      UID_HemodynamicWaveformStorage,
+      UID_ImplantAssemblyTemplateStorage,
+      UID_ImplantationPlanSRDocumentStorage,
+      UID_ImplantTemplateGroupStorage,
+      UID_IntraocularLensCalculationsStorage,
+      UID_KeratometryMeasurementsStorage,
+      UID_KeyObjectSelectionDocumentStorage,
+      UID_LensometryMeasurementsStorage,
+      UID_MacularGridThicknessAndVolumeReportStorage,
+      UID_MammographyCADSRStorage,
+      UID_MRImageStorage,
+      UID_MRSpectroscopyStorage,
+      UID_MultiframeGrayscaleByteSecondaryCaptureImageStorage,
+      UID_MultiframeGrayscaleWordSecondaryCaptureImageStorage,
+      UID_MultiframeSingleBitSecondaryCaptureImageStorage,
+      UID_MultiframeTrueColorSecondaryCaptureImageStorage,
+      UID_NuclearMedicineImageStorage,
+      UID_OphthalmicAxialMeasurementsStorage,
+      UID_OphthalmicPhotography16BitImageStorage,
+      UID_OphthalmicPhotography8BitImageStorage,
+      UID_OphthalmicTomographyImageStorage,
+      UID_OphthalmicVisualFieldStaticPerimetryMeasurementsStorage,
+      UID_PositronEmissionTomographyImageStorage,
+      UID_ProcedureLogStorage,
+      UID_PseudoColorSoftcopyPresentationStateStorage,
+      UID_RawDataStorage,
+      UID_RealWorldValueMappingStorage,
+      UID_RespiratoryWaveformStorage,
+      UID_RTBeamsTreatmentRecordStorage,
+      UID_RTBrachyTreatmentRecordStorage,
+      UID_RTDoseStorage,
+      UID_RTImageStorage,
+      UID_RTIonBeamsTreatmentRecordStorage,
+      UID_RTIonPlanStorage,
+      UID_RTPlanStorage,
+      UID_RTStructureSetStorage,
+      UID_RTTreatmentSummaryRecordStorage,
+      UID_SecondaryCaptureImageStorage,
+      UID_SegmentationStorage,
+      UID_SpatialFiducialsStorage,
+      UID_SpatialRegistrationStorage,
+      UID_SpectaclePrescriptionReportStorage,
+      UID_StereometricRelationshipStorage,
+      UID_SubjectiveRefractionMeasurementsStorage,
+      UID_SurfaceSegmentationStorage,
+      UID_TwelveLeadECGWaveformStorage,
+      UID_UltrasoundImageStorage,
+      UID_UltrasoundMultiframeImageStorage,
+      UID_VideoEndoscopicImageStorage,
+      UID_VideoMicroscopicImageStorage,
+      UID_VideoPhotographicImageStorage,
+      UID_VisualAcuityMeasurementsStorage,
+      UID_VLEndoscopicImageStorage,
+      UID_VLMicroscopicImageStorage,
+      UID_VLPhotographicImageStorage,
+      UID_VLSlideCoordinatesMicroscopicImageStorage,
+      UID_VLWholeSlideMicroscopyImageStorage,
+      UID_XAXRFGrayscaleSoftcopyPresentationStateStorage,
+      UID_XRay3DAngiographicImageStorage,
+      UID_XRay3DCraniofacialImageStorage,
+      UID_XRayAngiographicImageStorage,
+      UID_XRayRadiationDoseSRStorage,
+      UID_XRayRadiofluoroscopicImageStorage,
+      // retired
+      UID_RETIRED_HardcopyColorImageStorage,
+      UID_RETIRED_HardcopyGrayscaleImageStorage,
+      UID_RETIRED_NuclearMedicineImageStorage,
+      UID_RETIRED_StandaloneCurveStorage,
+      UID_RETIRED_StandaloneModalityLUTStorage,
+      UID_RETIRED_StandaloneOverlayStorage,
+      UID_RETIRED_StandalonePETCurveStorage,
+      UID_RETIRED_StandaloneVOILUTStorage,
+      UID_RETIRED_StoredPrintStorage,
+      UID_RETIRED_UltrasoundImageStorage,
+      UID_RETIRED_UltrasoundMultiframeImageStorage,
+      UID_RETIRED_VLImageStorage,
+      UID_RETIRED_VLMultiFrameImageStorage,
+      UID_RETIRED_XRayAngiographicBiPlaneImageStorage,
+      // draft
+      UID_DRAFT_SRAudioStorage,
+      UID_DRAFT_SRComprehensiveStorage,
+      UID_DRAFT_SRDetailStorage,
+      UID_DRAFT_SRTextStorage,
+      UID_DRAFT_WaveformStorage,
+      UID_DRAFT_RTBeamsDeliveryInstructionStorage,
+      NULL
+    };
+
+    const int orthancStorageSOPClassUIDsCount = (sizeof(orthancStorageSOPClassUIDs) / sizeof(const char*)) - 1;
+
+
+
     OFCondition AssociationCleanup(T_ASC_Association *assoc)
     {
       OFString temp_str;
@@ -313,7 +459,38 @@
         return NULL;
       }
 
-      LOG(INFO) << "Association Received";
+      // Retrieve the AET and the IP address of the remote modality
+      std::string callingAet;
+      std::string callingIp;
+      std::string calledAet;
+  
+      {
+        DIC_AE callingAet_C;
+        DIC_AE calledAet_C;
+        DIC_AE callingIp_C;
+        DIC_AE calledIP_C;
+        if (ASC_getAPTitles(assoc->params, callingAet_C, calledAet_C, NULL).bad() ||
+            ASC_getPresentationAddresses(assoc->params, callingIp_C, calledIP_C).bad())
+        {
+          T_ASC_RejectParameters rej =
+            {
+              ASC_RESULT_REJECTEDPERMANENT,
+              ASC_SOURCE_SERVICEUSER,
+              ASC_REASON_SU_NOREASON
+            };
+          ASC_rejectAssociation(assoc, &rej);
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+
+        callingIp = std::string(/*OFSTRING_GUARD*/(callingIp_C));
+        callingAet = std::string(/*OFSTRING_GUARD*/(callingAet_C));
+        calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
+      }
+
+      LOG(INFO) << "Association Received from AET " << callingAet 
+                << " on IP " << callingIp;
+
 
       std::vector<const char*> transferSyntaxes;
 
@@ -323,39 +500,75 @@
       transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
 
       // New transfer syntaxes supported since Orthanc 0.7.2
-      transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); 
-      transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax);
-      transferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax);
-      transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
-      transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
-      transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax);
-      transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax);
-      transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax);
-      transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax);
-      transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax);
-      transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax);
-      transferSyntaxes.push_back(UID_RLELosslessTransferSyntax);
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Deflated))
+      {
+        transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); 
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg))
+      {
+        transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax);
+        transferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax);
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg2000))
+      {
+        transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
+        transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_JpegLossless))
+      {
+        transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
+        transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
+        transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax);
+        transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax);
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpip))
+      {
+        transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax);
+        transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax);
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Mpeg2))
+      {
+        transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax);
+        transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax);
+      }
+
+      if (!server.HasApplicationEntityFilter() ||
+          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Rle))
+      {
+        transferSyntaxes.push_back(UID_RLELosslessTransferSyntax);
+      }
 
       /* accept the Verification SOP Class if presented */
-      cond = ASC_acceptContextsWithPreferredTransferSyntaxes( assoc->params, &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), &transferSyntaxes[0], transferSyntaxes.size());
+      cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), &transferSyntaxes[0], transferSyntaxes.size());
       if (cond.bad())
       {
         LOG(INFO) << cond.text();
@@ -364,7 +577,7 @@
       }
 
       /* the array of Storage SOP Class UIDs comes from dcuid.h */
-      cond = ASC_acceptContextsWithPreferredTransferSyntaxes( assoc->params, dcmAllStorageSOPClassUIDs, numberOfAllDcmStorageSOPClassUIDs, &transferSyntaxes[0], transferSyntaxes.size());
+      cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, orthancStorageSOPClassUIDs, orthancStorageSOPClassUIDsCount, &transferSyntaxes[0], transferSyntaxes.size());
       if (cond.bad())
       {
         LOG(INFO) << cond.text();
@@ -409,62 +622,38 @@
         return NULL;
       }
 
-      std::string callingIP;
-      std::string callingTitle;
-  
       /* check the AETs */
+      if (!server.IsMyAETitle(calledAet))
       {
-        DIC_AE callingTitle_C;
-        DIC_AE calledTitle_C;
-        DIC_AE callingIP_C;
-        DIC_AE calledIP_C;
-        if (ASC_getAPTitles(assoc->params, callingTitle_C, calledTitle_C, NULL).bad() ||
-            ASC_getPresentationAddresses(assoc->params, callingIP_C, calledIP_C).bad())
-        {
-          T_ASC_RejectParameters rej =
-            {
-              ASC_RESULT_REJECTEDPERMANENT,
-              ASC_SOURCE_SERVICEUSER,
-              ASC_REASON_SU_NOREASON
-            };
-          ASC_rejectAssociation(assoc, &rej);
-          AssociationCleanup(assoc);
-          return NULL;
-        }
-
-        callingIP = std::string(/*OFSTRING_GUARD*/(callingIP_C));
-        callingTitle = std::string(/*OFSTRING_GUARD*/(callingTitle_C));
-        std::string calledTitle(/*OFSTRING_GUARD*/(calledTitle_C));
-
-        if (!server.IsMyAETitle(calledTitle))
-        {
-          T_ASC_RejectParameters rej =
-            {
-              ASC_RESULT_REJECTEDPERMANENT,
-              ASC_SOURCE_SERVICEUSER,
-              ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED
-            };
-          ASC_rejectAssociation(assoc, &rej);
-          AssociationCleanup(assoc);
-          return NULL;
-        }
-
-        if (server.HasApplicationEntityFilter() &&
-            !server.GetApplicationEntityFilter().IsAllowedConnection(callingIP, callingTitle))
-        {
-          T_ASC_RejectParameters rej =
-            {
-              ASC_RESULT_REJECTEDPERMANENT,
-              ASC_SOURCE_SERVICEUSER,
-              ASC_REASON_SU_CALLINGAETITLENOTRECOGNIZED
-            };
-          ASC_rejectAssociation(assoc, &rej);
-          AssociationCleanup(assoc);
-          return NULL;
-        }
+        LOG(WARNING) << "Rejected association, because of a bad called AET in the request (" << calledAet << ")";
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED
+          };
+        ASC_rejectAssociation(assoc, &rej);
+        AssociationCleanup(assoc);
+        return NULL;
       }
 
-      if (opt_rejectWithoutImplementationUID && strlen(assoc->params->theirImplementationClassUID) == 0)
+      if (server.HasApplicationEntityFilter() &&
+          !server.GetApplicationEntityFilter().IsAllowedConnection(callingIp, callingAet))
+      {
+        LOG(WARNING) << "Rejected association for remote AET " << callingAet << " on IP " << callingIp;
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_CALLINGAETITLENOTRECOGNIZED
+          };
+        ASC_rejectAssociation(assoc, &rej);
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      if (opt_rejectWithoutImplementationUID && 
+          strlen(assoc->params->theirImplementationClassUID) == 0)
       {
         /* reject: the no implementation Class UID provided */
         T_ASC_RejectParameters rej =
@@ -498,7 +687,7 @@
       }
 
       IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL;
-      return new CommandDispatcher(server, assoc, callingIP, callingTitle, filter);
+      return new CommandDispatcher(server, assoc, callingIp, callingAet, filter);
     }
 
     bool CommandDispatcher::Step()
@@ -580,14 +769,16 @@
 
         // Check whether this request is allowed by the security filter
         if (supported && 
+            request != DicomRequestType_Echo &&  // Always allow incoming ECHO requests
             filter_ != NULL &&
             !filter_->IsAllowedRequest(callingIP_, callingAETitle_, request))
         {
           LOG(ERROR) << EnumerationToString(request) 
                      << " requests are disallowed for the AET \"" 
                      << callingAETitle_ << "\"";
-          cond = DIMSE_BADCOMMANDTYPE;
+          cond = DIMSE_ILLEGALASSOCIATION;
           supported = false;
+          finished = true;
         }
 
         // in case we received a supported message, process this command
@@ -668,7 +859,7 @@
     }
 
 
-    OFCondition EchoScp( T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID)
+    OFCondition EchoScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID)
     {
       OFString temp_str;
       LOG(INFO) << "Received Echo Request";
--- a/OrthancServer/Internals/CommandDispatcher.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/Internals/CommandDispatcher.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/Internals/DicomImageDecoder.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/Internals/DicomImageDecoder.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -225,7 +225,6 @@
   private:
     std::string psmct_;
     std::auto_ptr<DicomIntegerPixelAccessor> slowAccessor_;
-    std::auto_ptr<ImageAccessor> fastAccessor_;
 
   public:
     void Setup(DcmDataset& dataset,
@@ -233,7 +232,6 @@
     {
       psmct_.clear();
       slowAccessor_.reset(NULL);
-      fastAccessor_.reset(NULL);
 
       // See also: http://support.dcmtk.org/wiki/dcmtk/howto/accessing-compressed-data
 
@@ -272,13 +270,6 @@
       }
 
       slowAccessor_->SetCurrentFrame(frame);
-
-
-      /**
-       * If possible, create a fast ImageAccessor to the image buffer.
-       **/
-
-      
     }
 
     unsigned int GetWidth() const
@@ -305,15 +296,10 @@
       return *slowAccessor_;
     }
 
-    bool HasFastAccessor() const
+    unsigned int GetSize() const
     {
-      return fastAccessor_.get() != NULL;
-    }
-
-    const ImageAccessor& GetFastAccessor() const
-    {
-      assert(HasFastAccessor());
-      return *fastAccessor_;
+      assert(slowAccessor_.get() != NULL);
+      return slowAccessor_->GetSize();
     }
   };
 
@@ -332,7 +318,9 @@
       LOG(WARNING) << "Unsupported DICOM image: " << info.GetBitsStored() 
                    << "bpp, " << info.GetChannelCount() << " channels, " 
                    << (info.IsSigned() ? "signed" : "unsigned")
-                   << (info.IsPlanar() ? ", planar" : ", non-planar");
+                   << (info.IsPlanar() ? ", planar, " : ", non-planar, ")
+                   << EnumerationToString(info.GetPhotometricInterpretation())
+                   << " photometric interpretation";
       throw OrthancException(ErrorCode_NotImplemented);
     }
 
@@ -435,16 +423,22 @@
     {
       try
       {
-        ImageAccessor sourceImage;
-        sourceImage.AssignReadOnly(sourceFormat, 
-                                   info.GetWidth(), 
-                                   info.GetHeight(),
-                                   info.GetWidth() * GetBytesPerPixel(sourceFormat),
-                                   source.GetAccessor().GetPixelData());                                   
+        size_t frameSize = info.GetHeight() * info.GetWidth() * GetBytesPerPixel(sourceFormat);
+        if ((frame + 1) * frameSize <= source.GetSize())
+        {
+          const uint8_t* buffer = reinterpret_cast<const uint8_t*>(source.GetAccessor().GetPixelData());
 
-        ImageProcessing::Convert(targetAccessor, sourceImage);
-        ImageProcessing::ShiftRight(targetAccessor, info.GetShift());
-        fastVersionSuccess = true;
+          ImageAccessor sourceImage;
+          sourceImage.AssignReadOnly(sourceFormat, 
+                                     info.GetWidth(), 
+                                     info.GetHeight(),
+                                     info.GetWidth() * GetBytesPerPixel(sourceFormat),
+                                     buffer + frame * frameSize);
+
+          ImageProcessing::Convert(targetAccessor, sourceImage);
+          ImageProcessing::ShiftRight(targetAccessor, info.GetShift());
+          fastVersionSuccess = true;
+        }
       }
       catch (OrthancException&)
       {
@@ -452,7 +446,6 @@
       }
     }
 
-
     /**
      * Slow version : loop over the DICOM buffer, storing its value
      * into the target image.
@@ -589,10 +582,18 @@
   }
 
 
+  static bool IsColorImage(PixelFormat format)
+  {
+    return (format == PixelFormat_RGB24 ||
+            format == PixelFormat_RGBA32);
+  }
+
+
   bool DicomImageDecoder::DecodeAndTruncate(ImageBuffer& target,
                                             DcmDataset& dataset,
                                             unsigned int frame,
-                                            PixelFormat format)
+                                            PixelFormat format,
+                                            bool allowColorConversion)
   {
     // TODO Special case for uncompressed images
     
@@ -602,6 +603,19 @@
       return false;
     }
 
+    // If specified, prevent the conversion between color and
+    // grayscale images
+    bool isSourceColor = IsColorImage(source.GetFormat());
+    bool isTargetColor = IsColorImage(format);
+
+    if (!allowColorConversion)
+    {
+      if (isSourceColor ^ isTargetColor)
+      {
+        return false;
+      }
+    }
+
     if (source.GetFormat() == format)
     {
       // No conversion is required, return the temporary image
--- a/OrthancServer/Internals/DicomImageDecoder.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/Internals/DicomImageDecoder.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -72,7 +72,8 @@
     static bool DecodeAndTruncate(ImageBuffer& target,
                                   DcmDataset& dataset,
                                   unsigned int frame,
-                                  PixelFormat format);
+                                  PixelFormat format,
+                                  bool allowColorConversion);
 
     static bool DecodePreview(ImageBuffer& target,
                               DcmDataset& dataset,
--- a/OrthancServer/Internals/FindScp.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/Internals/FindScp.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/Internals/FindScp.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/Internals/FindScp.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/Internals/MoveScp.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/Internals/MoveScp.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/Internals/MoveScp.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/Internals/MoveScp.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/Internals/StoreScp.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/Internals/StoreScp.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -102,7 +102,8 @@
     struct StoreCallbackData
     {
       IStoreRequestHandler* handler;
-      const char* distantAET;
+      const char* remoteAET;
+      const char* calledAET;
       const char* modality;
       const char* affectedSOPInstanceUID;
       uint32_t messageID;
@@ -168,7 +169,7 @@
             FromDcmtkBridge::Convert(summary, **imageDataSet);
             FromDcmtkBridge::ToJson(dicomJson, **imageDataSet);       
 
-            if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, *imageDataSet))
+            if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, **imageDataSet))
             {
               LOG(ERROR) << "cannot write DICOM file to memory";
               rsp->DimseStatus = STATUS_STORE_Refused_OutOfResources;
@@ -181,7 +182,7 @@
 
           // check the image to make sure it is consistent, i.e. that its sopClass and sopInstance correspond
           // to those mentioned in the request. If not, set the status in the response message variable.
-          if ((rsp->DimseStatus == STATUS_Success))
+          if (rsp->DimseStatus == STATUS_Success)
           {
             // which SOP class and SOP instance ?
             if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sopInstance, /*opt_correctUIDPadding*/ OFFalse))
@@ -201,7 +202,7 @@
             {
               try
               {
-                cbdata->handler->Handle(buffer, summary, dicomJson, cbdata->distantAET);
+                cbdata->handler->Handle(buffer, summary, dicomJson, cbdata->remoteAET, cbdata->calledAET);
               }
               catch (OrthancException& e)
               {
@@ -255,11 +256,13 @@
     callbackData.messageID = req->MessageID;
     if (assoc && assoc->params)
     {
-      callbackData.distantAET = assoc->params->DULparams.callingAPTitle;
+      callbackData.remoteAET = assoc->params->DULparams.callingAPTitle;
+      callbackData.calledAET = assoc->params->DULparams.calledAPTitle;
     }
     else
     {
-      callbackData.distantAET = "";
+      callbackData.remoteAET = "";
+      callbackData.calledAET = "";
     }
 
     DcmFileFormat dcmff;
--- a/OrthancServer/Internals/StoreScp.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/Internals/StoreScp.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -41,160 +41,12 @@
 #include "OrthancInitialization.h"
 #include "FromDcmtkBridge.h"
 
-namespace Orthanc
-{
-  static bool IsWildcard(const std::string& constraint)
-  {
-    return (constraint.find('-') != std::string::npos ||
-            constraint.find('*') != std::string::npos ||
-            constraint.find('\\') != std::string::npos ||
-            constraint.find('?') != std::string::npos);
-  }
-
-  static bool ApplyRangeConstraint(const std::string& value,
-                                   const std::string& constraint)
-  {
-    size_t separator = constraint.find('-');
-    std::string lower, upper, v;
-    Toolbox::ToLowerCase(lower, constraint.substr(0, separator));
-    Toolbox::ToLowerCase(upper, constraint.substr(separator + 1));
-    Toolbox::ToLowerCase(v, value);
-
-    if (lower.size() == 0 && upper.size() == 0)
-    {
-      return false;
-    }
-
-    if (lower.size() == 0)
-    {
-      return v <= upper;
-    }
-
-    if (upper.size() == 0)
-    {
-      return v >= lower;
-    }
-    
-    return (v >= lower && v <= upper);
-  }
-
-
-  static bool ApplyListConstraint(const std::string& value,
-                                  const std::string& constraint)
-  {
-    std::string v1;
-    Toolbox::ToLowerCase(v1, value);
-
-    std::vector<std::string> items;
-    Toolbox::TokenizeString(items, constraint, '\\');
-
-    for (size_t i = 0; i < items.size(); i++)
-    {
-      std::string lower;
-      Toolbox::ToLowerCase(lower, items[i]);
-      if (lower == v1)
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
+#include "ResourceFinder.h"
+#include "DicomFindQuery.h"
 
 
-  static bool Matches(const std::string& value,
-                      const std::string& constraint)
-  {
-    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
-    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
-
-    if (constraint.find('-') != std::string::npos)
-    {
-      return ApplyRangeConstraint(value, constraint);
-    }
-    
-    if (constraint.find('\\') != std::string::npos)
-    {
-      return ApplyListConstraint(value, constraint);
-    }
-
-    if (constraint.find('*') != std::string::npos ||
-        constraint.find('?') != std::string::npos)
-    {
-      // TODO - Cache the constructed regular expression
-      boost::regex pattern(Toolbox::WildcardToRegularExpression(constraint),
-                           boost::regex::icase /* case insensitive search */);
-      return boost::regex_match(value, pattern);
-    }
-    else
-    {
-      std::string v, c;
-      Toolbox::ToLowerCase(v, value);
-      Toolbox::ToLowerCase(c, constraint);
-      return v == c;
-    }
-  }
-
-
-  static bool LookupOneInstance(std::string& result,
-                                ServerIndex& index,
-                                const std::string& id,
-                                ResourceType type)
-  {
-    if (type == ResourceType_Instance)
-    {
-      result = id;
-      return true;
-    }
-
-    std::string childId;
-    
-    {
-      std::list<std::string> children;
-      index.GetChildInstances(children, id);
-
-      if (children.empty())
-      {
-        return false;
-      }
-
-      childId = children.front();
-    }
-
-    return LookupOneInstance(result, index, childId, GetChildResourceType(type));
-  }
-
-
-  static bool Matches(const Json::Value& resource,
-                      const DicomArray& query)
-  {
-    for (size_t i = 0; i < query.GetSize(); i++)
-    {
-      if (query.GetElement(i).GetValue().IsNull() ||
-          query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
-          query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET ||
-          query.GetElement(i).GetTag() == DICOM_TAG_MODALITIES_IN_STUDY)
-      {
-        continue;
-      }
-
-      std::string tag = query.GetElement(i).GetTag().Format();
-      std::string value;
-      if (resource.isMember(tag))
-      {
-        value = resource.get(tag, Json::arrayValue).get("Value", "").asString();
-      }
-
-      if (!Matches(value, query.GetElement(i).GetValue().AsString()))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-
+namespace Orthanc
+{
   static void AddAnswer(DicomFindAnswers& answers,
                         const Json::Value& resource,
                         const DicomArray& query)
@@ -203,8 +55,15 @@
 
     for (size_t i = 0; i < query.GetSize(); i++)
     {
-      if (query.GetElement(i).GetTag() != DICOM_TAG_QUERY_RETRIEVE_LEVEL &&
-          query.GetElement(i).GetTag() != DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      // Fix issue 30 (QR response missing "Query/Retrieve Level" (008,0052))
+      if (query.GetElement(i).GetTag() == DICOM_TAG_QUERY_RETRIEVE_LEVEL)
+      {
+        result.SetValue(query.GetElement(i).GetTag(), query.GetElement(i).GetValue());
+      }
+      else if (query.GetElement(i).GetTag() == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+      }
+      else
       {
         std::string tag = query.GetElement(i).GetTag().Format();
         std::string value;
@@ -220,267 +79,157 @@
       }
     }
 
-    answers.Add(result);
-  }
-
-
-  static bool ApplyModalitiesInStudyFilter(std::list<std::string>& filteredStudies,
-                                           const std::list<std::string>& studies,
-                                           const DicomMap& input,
-                                           ServerIndex& index)
-  {
-    filteredStudies.clear();
-
-    const DicomValue& v = input.GetValue(DICOM_TAG_MODALITIES_IN_STUDY);
-    if (v.IsNull())
-    {
-      return false;
-    }
-
-    // Move the allowed modalities into a "std::set"
-    std::vector<std::string>  tmp;
-    Toolbox::TokenizeString(tmp, v.AsString(), '\\'); 
-
-    std::set<std::string> modalities;
-    for (size_t i = 0; i < tmp.size(); i++)
-    {
-      modalities.insert(tmp[i]);
-    }
-
-    // Loop over the studies
-    for (std::list<std::string>::const_iterator 
-           it = studies.begin(); it != studies.end(); ++it)
+    if (result.GetSize() == 0)
     {
-      try
-      {
-        // We are considering a single study. Check whether one of
-        // its child series matches one of the modalities.
-        Json::Value study;
-        if (index.LookupResource(study, *it, ResourceType_Study))
-        {
-          // Loop over the series of the considered study.
-          for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++)   // (*)
-          {
-            Json::Value series;
-            if (index.LookupResource(series, study["Series"][j].asString(), ResourceType_Series))
-            {
-              // Get the modality of this series
-              if (series["MainDicomTags"].isMember("Modality"))
-              {
-                std::string modality = series["MainDicomTags"]["Modality"].asString();
-                if (modalities.find(modality) != modalities.end())
-                {
-                  // This series of the considered study matches one
-                  // of the required modalities. Take the study into
-                  // consideration for future filtering.
-                  filteredStudies.push_back(*it);
-
-                  // We have finished considering this study. Break the study loop at (*).
-                  break;
-                }
-              }
-            }
-          }
-        }
-      }
-      catch (OrthancException&)
-      {
-        // This resource has probably been deleted during the find request
-      }
+      LOG(WARNING) << "The C-FIND request does not return any DICOM tag";
     }
-
-    return true;
+    else
+    {
+      answers.Add(result);
+    }
   }
 
 
   namespace
   {
-    class CandidateResources
+    class CFindQuery : public DicomFindQuery
     {
     private:
-      ServerIndex&  index_;
-      ModalityManufacturer manufacturer_;
-      ResourceType  level_;
-      bool  isFilterApplied_;
-      std::set<std::string>  filtered_;
-
-      static void ListToSet(std::set<std::string>& target,
-                            const std::list<std::string>& source)
-      {
-        for (std::list<std::string>::const_iterator
-               it = source.begin(); it != source.end(); ++it)
-        {
-          target.insert(*it);
-        }
-      }
-
-      void ApplyExactFilter(const DicomTag& tag, const std::string& value)
-      {
-        LOG(INFO) << "Applying exact filter on tag "
-                  << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")";
-
-        std::list<std::string> resources;
-        index_.LookupTagValue(resources, tag, value, level_);
-
-        if (isFilterApplied_)
-        {
-          std::set<std::string>  s;
-          ListToSet(s, resources);
-
-          std::set<std::string> tmp = filtered_;
-          filtered_.clear();
-
-          for (std::set<std::string>::const_iterator 
-                 it = tmp.begin(); it != tmp.end(); ++it)
-          {
-            if (s.find(*it) != s.end())
-            {
-              filtered_.insert(*it);
-            }
-          }
-        }
-        else
-        {
-          assert(filtered_.empty());
-          isFilterApplied_ = true;
-          ListToSet(filtered_, resources);
-        }
-      }
+      DicomFindAnswers&      answers_;
+      ServerIndex&           index_;
+      const DicomArray&      query_;
+      bool                   hasModalitiesInStudy_;
+      std::set<std::string>  modalitiesInStudy_;
 
     public:
-      CandidateResources(ServerIndex& index,
-                         ModalityManufacturer manufacturer) : 
-        index_(index), 
-        manufacturer_(manufacturer),
-        level_(ResourceType_Patient), 
-        isFilterApplied_(false)
+      CFindQuery(DicomFindAnswers& answers,
+                 ServerIndex& index,
+                 const DicomArray& query) :
+        answers_(answers),
+        index_(index),
+        query_(query),
+        hasModalitiesInStudy_(false)
       {
       }
 
-      ResourceType GetLevel() const
-      {
-        return level_;
-      }
-
-      void GoDown()
+      void SetModalitiesInStudy(const std::string& value)
       {
-        assert(level_ != ResourceType_Instance);
-
-        if (isFilterApplied_)
-        {
-          std::set<std::string> tmp = filtered_;
-
-          filtered_.clear();
+        hasModalitiesInStudy_ = true;
+        
+        std::vector<std::string>  tmp;
+        Toolbox::TokenizeString(tmp, value, '\\'); 
 
-          for (std::set<std::string>::const_iterator 
-                 it = tmp.begin(); it != tmp.end(); ++it)
-          {
-            std::list<std::string> children;
-            index_.GetChildren(children, *it);
-            ListToSet(filtered_, children);
-          }
-        }
-
-        switch (level_)
+        for (size_t i = 0; i < tmp.size(); i++)
         {
-          case ResourceType_Patient:
-            level_ = ResourceType_Study;
-            break;
-
-          case ResourceType_Study:
-            level_ = ResourceType_Series;
-            break;
-
-          case ResourceType_Series:
-            level_ = ResourceType_Instance;
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_InternalError);
+          modalitiesInStudy_.insert(tmp[i]);
         }
       }
 
-      void Flatten(std::list<std::string>& resources) const
+      virtual bool HasMainDicomTagsFilter(ResourceType level) const
       {
-        resources.clear();
-
-        if (isFilterApplied_)
+        if (DicomFindQuery::HasMainDicomTagsFilter(level))
         {
-          for (std::set<std::string>::const_iterator 
-                 it = filtered_.begin(); it != filtered_.end(); ++it)
-          {
-            resources.push_back(*it);
-          }
+          return true;
         }
-        else
-        {
-          Json::Value tmp;
-          index_.GetAllUuids(tmp, level_);
-          for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
-          {
-            resources.push_back(tmp[i].asString());
-          }
-        }
+
+        return (level == ResourceType_Study &&
+                hasModalitiesInStudy_);
       }
 
-      void ApplyFilter(const DicomTag& tag, const DicomMap& query)
+      virtual bool FilterMainDicomTags(const std::string& resourceId,
+                                       ResourceType level,
+                                       const DicomMap& mainTags) const
       {
-        if (query.HasTag(tag))
+        if (!DicomFindQuery::FilterMainDicomTags(resourceId, level, mainTags))
+        {
+          return false;
+        }
+
+        if (level != ResourceType_Study ||
+            !hasModalitiesInStudy_)
+        {
+          return true;
+        }
+
+        try
         {
-          const DicomValue& value = query.GetValue(tag);
-          if (!value.IsNull())
+          // We are considering a single study, and the
+          // "MODALITIES_IN_STUDY" tag is set in the C-Find. Check
+          // whether one of its child series matches one of the
+          // modalities.
+
+          Json::Value study;
+          if (index_.LookupResource(study, resourceId, ResourceType_Study))
           {
-            std::string value = query.GetValue(tag).AsString();
-            if (!IsWildcard(value))
+            // Loop over the series of the considered study.
+            for (Json::Value::ArrayIndex j = 0; j < study["Series"].size(); j++)
             {
-              ApplyExactFilter(tag, value);
+              Json::Value series;
+              if (index_.LookupResource(series, study["Series"][j].asString(), ResourceType_Series))
+              {
+                // Get the modality of this series
+                if (series["MainDicomTags"].isMember("Modality"))
+                {
+                  std::string modality = series["MainDicomTags"]["Modality"].asString();
+                  if (modalitiesInStudy_.find(modality) != modalitiesInStudy_.end())
+                  {
+                    // This series of the considered study matches one
+                    // of the required modalities. Take the study into
+                    // consideration for future filtering.
+                    return true;
+                  }
+                }
+              }
             }
           }
         }
+        catch (OrthancException&)
+        {
+          // This resource has probably been deleted during the find request
+        }
+
+        return false;
+      }
+
+      virtual bool HasInstanceFilter() const
+      {
+        return true;
+      }
+
+      virtual bool FilterInstance(const std::string& instanceId,
+                                  const Json::Value& content) const
+      {
+        bool ok = DicomFindQuery::FilterInstance(instanceId, content);
+
+        if (ok)
+        {
+          // Add this resource to the answers
+          AddAnswer(answers_, content, query_);
+        }
+
+        return ok;
       }
     };
   }
 
 
-  bool OrthancFindRequestHandler::HasReachedLimit(const DicomFindAnswers& answers,
-                                                  ResourceType level) const
-  {
-    switch (level)
-    {
-      case ResourceType_Patient:
-      case ResourceType_Study:
-      case ResourceType_Series:
-        return (maxResults_ != 0 && answers.GetSize() >= maxResults_);
-
-      case ResourceType_Instance:
-        return (maxInstances_ != 0 && answers.GetSize() >= maxInstances_);
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
 
   bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
                                          const DicomMap& input,
                                          const std::string& callingAETitle)
   {
     /**
-     * Retrieve the manufacturer of this modality.
+     * Ensure that the calling modality is known to Orthanc.
      **/
 
-    ModalityManufacturer manufacturer;
-
-    {
-      RemoteModalityParameters modality;
+    RemoteModalityParameters modality;
 
-      if (!Configuration::LookupDicomModalityUsingAETitle(modality, callingAETitle))
-      {
-        throw OrthancException("Unknown modality");
-      }
+    if (!Configuration::LookupDicomModalityUsingAETitle(modality, callingAETitle))
+    {
+      throw OrthancException("Unknown modality");
+    }
 
-      manufacturer = modality.GetManufacturer();
-    }
+    // ModalityManufacturer manufacturer = modality.GetManufacturer();
 
 
     /**
@@ -519,114 +268,63 @@
 
 
     /**
-     * Retrieve the candidate resources for this query level. Whenever
-     * possible, we avoid returning ALL the resources for this query
-     * level, as it would imply reading the JSON file on the harddisk
-     * for each of them.
+     * Build up the query object.
      **/
 
-    CandidateResources candidates(context_.GetIndex(), manufacturer);
-
-    for (;;)
+    CFindQuery findQuery(answers, context_.GetIndex(), query);
+    findQuery.SetLevel(level);
+        
+    for (size_t i = 0; i < query.GetSize(); i++)
     {
-      switch (candidates.GetLevel())
-      {
-        case ResourceType_Patient:
-          candidates.ApplyFilter(DICOM_TAG_PATIENT_ID, input);
-          break;
-
-        case ResourceType_Study:
-          candidates.ApplyFilter(DICOM_TAG_STUDY_INSTANCE_UID, input);
-          candidates.ApplyFilter(DICOM_TAG_ACCESSION_NUMBER, input);
-          break;
+      const DicomTag tag = query.GetElement(i).GetTag();
 
-        case ResourceType_Series:
-          candidates.ApplyFilter(DICOM_TAG_SERIES_INSTANCE_UID, input);
-          break;
-
-        case ResourceType_Instance:
-          candidates.ApplyFilter(DICOM_TAG_SOP_INSTANCE_UID, input);
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }      
-
-      if (candidates.GetLevel() == level)
+      if (query.GetElement(i).GetValue().IsNull() ||
+          tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
+          tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
       {
-        break;
+        continue;
       }
 
-      candidates.GoDown();
-    }
-
-    std::list<std::string>  resources;
-    candidates.Flatten(resources);
-
-    LOG(INFO) << "Number of candidate resources after exact filtering: " << resources.size();
+      std::string value = query.GetElement(i).GetValue().AsString();
 
-    /**
-     * Apply filtering on modalities for studies, if asked (this is an
-     * extension to standard DICOM)
-     * http://www.medicalconnections.co.uk/kb/Filtering_on_and_Retrieving_the_Modality_in_a_C_FIND
-     **/
-
-    if (level == ResourceType_Study &&
-        input.HasTag(DICOM_TAG_MODALITIES_IN_STUDY))
-    {
-      std::list<std::string> filtered;
-      if (ApplyModalitiesInStudyFilter(filtered, resources, input, context_.GetIndex()))
+      if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
       {
-        resources = filtered;
+        findQuery.SetModalitiesInStudy(value);
+      }
+      else
+      {
+        findQuery.SetConstraint(tag, value);
       }
     }
 
 
     /**
-     * Loop over all the resources for this query level.
+     * Run the query.
      **/
 
-    for (std::list<std::string>::const_iterator 
-           resource = resources.begin(); resource != resources.end(); ++resource)
+    ResourceFinder finder(context_);
+
+    switch (level)
     {
-      try
-      {
-        std::string instance;
-        if (LookupOneInstance(instance, context_.GetIndex(), *resource, level))
-        {
-          Json::Value info;
-          context_.ReadJson(info, instance);
-        
-          if (Matches(info, query))
-          {
-            if (HasReachedLimit(answers, level))
-            {
-              // Too many results, stop before recording this new match
-              return false;
-            }
+      case ResourceType_Patient:
+      case ResourceType_Study:
+      case ResourceType_Series:
+        finder.SetMaxResults(maxResults_);
+        break;
 
-            AddAnswer(answers, info, query);
-          }
-        }
-      }
-      catch (OrthancException&)
-      {
-        // This resource has probably been deleted during the find request
-      }
+      case ResourceType_Instance:
+        finder.SetMaxResults(maxInstances_);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
     }
 
-    return true;  // All the matching resources have been returned
+    std::list<std::string> tmp;
+    bool finished = finder.Apply(tmp, findQuery);
+
+    LOG(INFO) << "Number of matching resources: " << tmp.size();
+
+    return finished;
   }
 }
-
-
-
-/**
- * TODO : Case-insensitive match for PN value representation (Patient
- * Name). Case-senstive match for all the other value representations.
- *
- * Reference: DICOM PS 3.4
- *   - C.2.2.2.1 ("Single Value Matching") 
- *   - C.2.2.2.4 ("Wild Card Matching")
- * http://medical.nema.org/Dicom/2011/11_04pu.pdf (
- **/
--- a/OrthancServer/OrthancFindRequestHandler.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancFindRequestHandler.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/OrthancInitialization.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancInitialization.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -45,6 +45,9 @@
 #include <boost/thread.hpp>
 #include <glog/logging.h>
 
+#include "DatabaseWrapper.h"
+#include "../Core/FileStorage/FilesystemStorage.h"
+
 
 #if ORTHANC_JPEG_ENABLED == 1
 #include <dcmtk/dcmjpeg/djdecode.h>
@@ -61,6 +64,7 @@
   static boost::mutex globalMutex_;
   static std::auto_ptr<Json::Value> configuration_;
   static boost::filesystem::path defaultDirectory_;
+  static std::string configurationAbsolutePath_;
 
 
   static void ReadGlobalConfiguration(const char* configurationFile)
@@ -74,6 +78,7 @@
       Toolbox::ReadFile(content, configurationFile);
       defaultDirectory_ = boost::filesystem::path(configurationFile).parent_path();
       LOG(WARNING) << "Using the configuration from: " << configurationFile;
+      configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).string();
     }
     else
     {
@@ -93,6 +98,7 @@
         p /= "Configuration.json";
         Toolbox::ReadFile(content, p.string());
         LOG(WARNING) << "Using the configuration from: " << p.string();
+        configurationAbsolutePath_ = boost::filesystem::absolute(p).string();
       }
       catch (OrthancException&)
       {
@@ -103,6 +109,7 @@
 #endif
     }
 
+
     Json::Reader reader;
     if (!reader.parse(content, *configuration_))
     {
@@ -288,7 +295,8 @@
     if (modalities.type() != Json::objectValue ||
         !modalities.isMember(name))
     {
-      throw OrthancException(ErrorCode_BadFileFormat);
+      LOG(ERROR) << "No modality with symbolic name: " << name;
+      throw OrthancException(ErrorCode_InexistentItem);
     }
 
     try
@@ -321,7 +329,8 @@
       if (modalities.type() != Json::objectValue ||
           !modalities.isMember(name))
       {
-        throw OrthancException(ErrorCode_BadFileFormat);
+        LOG(ERROR) << "No peer with symbolic name: " << name;
+        throw OrthancException(ErrorCode_InexistentItem);
       }
 
       peer.FromJson(modalities[name]);
@@ -641,4 +650,115 @@
 
     peers.removeMember(symbolicName.c_str());
   }
+
+
+  
+  const std::string& Configuration::GetConfigurationAbsolutePath()
+  {
+    return configurationAbsolutePath_;
+  }
+
+
+  static IDatabaseWrapper* CreateSQLiteWrapper()
+  {
+    std::string storageDirectoryStr = Configuration::GetGlobalStringParameter("StorageDirectory", "OrthancStorage");
+
+    // Open the database
+    boost::filesystem::path indexDirectory = Configuration::InterpretStringParameterAsPath(
+      Configuration::GetGlobalStringParameter("IndexDirectory", storageDirectoryStr));
+
+    LOG(WARNING) << "SQLite index directory: " << indexDirectory;
+
+    try
+    {
+      boost::filesystem::create_directories(indexDirectory);
+    }
+    catch (boost::filesystem::filesystem_error)
+    {
+    }
+
+    return new DatabaseWrapper(indexDirectory.string() + "/index");
+  }
+
+
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+
+    class FilesystemStorageWithoutDicom : public IStorageArea
+    {
+    private:
+      FilesystemStorage storage_;
+
+    public:
+      FilesystemStorageWithoutDicom(const std::string& path) : storage_(path)
+      {
+      }
+
+      virtual void Create(const std::string& uuid,
+                          const void* content, 
+                          size_t size,
+                          FileContentType type)
+      {
+        if (type != FileContentType_Dicom)
+        {
+          storage_.Create(uuid, content, size, type);
+        }
+      }
+
+      virtual void Read(std::string& content,
+                        const std::string& uuid,
+                        FileContentType type)
+      {
+        if (type != FileContentType_Dicom)
+        {
+          storage_.Read(content, uuid, type);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+      }
+
+      virtual void Remove(const std::string& uuid,
+                          FileContentType type) 
+      {
+        if (type != FileContentType_Dicom)
+        {
+          storage_.Remove(uuid, type);
+        }
+      }
+    };
+  }
+
+
+  static IStorageArea* CreateFilesystemStorage()
+  {
+    std::string storageDirectoryStr = Configuration::GetGlobalStringParameter("StorageDirectory", "OrthancStorage");
+
+    boost::filesystem::path storageDirectory = Configuration::InterpretStringParameterAsPath(storageDirectoryStr);
+    LOG(WARNING) << "Storage directory: " << storageDirectory;
+
+    if (Configuration::GetGlobalBoolParameter("StoreDicom", true))
+    {
+      return new FilesystemStorage(storageDirectory.string());
+    }
+    else
+    {
+      LOG(WARNING) << "The DICOM files will not be stored, Orthanc running in index-only mode";
+      return new FilesystemStorageWithoutDicom(storageDirectory.string());
+    }
+  }
+
+
+  IDatabaseWrapper* Configuration::CreateDatabaseWrapper()
+  {
+    return CreateSQLiteWrapper();
+  }
+
+
+  IStorageArea* Configuration::CreateStorageArea()
+  {
+    return CreateFilesystemStorage();
+  }  
 }
--- a/OrthancServer/OrthancInitialization.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancInitialization.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -40,6 +40,8 @@
 #include "DicomProtocol/RemoteModalityParameters.h"
 #include "ServerEnumerations.h"
 #include "OrthancPeerParameters.h"
+#include "IDatabaseWrapper.h"
+#include "../Core/FileStorage/IStorageArea.h"
 
 namespace Orthanc
 {
@@ -100,5 +102,11 @@
                            const OrthancPeerParameters& peer);
 
     static void RemovePeer(const std::string& symbolicName);
+
+    static const std::string& GetConfigurationAbsolutePath();
+
+    static IDatabaseWrapper* CreateDatabaseWrapper();
+
+    static IStorageArea* CreateStorageArea();
   };
 }
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -101,9 +101,9 @@
   }
 
 
-  bool OrthancMoveRequestHandler::LookupResource(std::string& publicId,
-                                                 DicomTag tag,
-                                                 const DicomMap& input)
+  bool OrthancMoveRequestHandler::LookupIdentifier(std::string& publicId,
+                                                   DicomTag tag,
+                                                   const DicomMap& input)
   {
     if (!input.HasTag(tag))
     {
@@ -113,7 +113,7 @@
     std::string value = input.GetValue(tag).AsString();
 
     std::list<std::string> ids;
-    context_.GetIndex().LookupTagValue(ids, tag, value);
+    context_.GetIndex().LookupIdentifier(ids, tag, value);
 
     if (ids.size() != 1)
     {
@@ -132,18 +132,42 @@
   {
     LOG(WARNING) << "Move-SCU request received for AET \"" << aet << "\"";
 
-
     /**
      * Retrieve the query level.
      **/
 
+    ResourceType level;
     const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-    if (levelTmp == NULL) 
+
+    if (levelTmp != NULL) 
+    {
+      level = StringToResourceType(levelTmp->AsString().c_str());
+    }
+    else
     {
-      throw OrthancException(ErrorCode_BadRequest);
+      // The query level is not present in the C-Move request, which
+      // does not follow the DICOM standard. This is for instance the
+      // behavior of Tudor DICOM. Try and automatically deduce the
+      // query level: Start from the instance level, going up to the
+      // patient level until a valid DICOM identifier is found.
+
+      std::string publicId;
+
+      if (LookupIdentifier(publicId, DICOM_TAG_SOP_INSTANCE_UID, input) ||
+          LookupIdentifier(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input) ||
+          LookupIdentifier(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input) ||
+          LookupIdentifier(publicId, DICOM_TAG_PATIENT_ID, input))
+      {
+        return new OrthancMoveRequestIterator(context_, aet, publicId);
+      }
+      else
+      {
+        // No identifier is present in the request.
+        throw OrthancException(ErrorCode_BadRequest);
+      }
     }
 
-    ResourceType level = StringToResourceType(levelTmp->AsString().c_str());
+
 
     /**
      * Lookup for the resource to be sent.
@@ -155,19 +179,19 @@
     switch (level)
     {
       case ResourceType_Patient:
-        ok = LookupResource(publicId, DICOM_TAG_PATIENT_ID, input);
+        ok = LookupIdentifier(publicId, DICOM_TAG_PATIENT_ID, input);
         break;
 
       case ResourceType_Study:
-        ok = LookupResource(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input);
+        ok = LookupIdentifier(publicId, DICOM_TAG_STUDY_INSTANCE_UID, input);
         break;
 
       case ResourceType_Series:
-        ok = LookupResource(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input);
+        ok = LookupIdentifier(publicId, DICOM_TAG_SERIES_INSTANCE_UID, input);
         break;
 
       case ResourceType_Instance:
-        ok = LookupResource(publicId, DICOM_TAG_SOP_INSTANCE_UID, input);
+        ok = LookupIdentifier(publicId, DICOM_TAG_SOP_INSTANCE_UID, input);
         break;
 
       default:
--- a/OrthancServer/OrthancMoveRequestHandler.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -41,9 +41,9 @@
   private:
     ServerContext& context_;
 
-    bool LookupResource(std::string& publicId,
-                        DicomTag tag,
-                        const DicomMap& input);
+    bool LookupIdentifier(std::string& publicId,
+                          DicomTag tag,
+                          const DicomMap& input);
 
   public:
     OrthancMoveRequestHandler(ServerContext& context) :
--- a/OrthancServer/OrthancPeerParameters.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancPeerParameters.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/OrthancPeerParameters.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancPeerParameters.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,8 +33,8 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
-#include "../DicomModification.h"
 #include "../FromDcmtkBridge.h"
+#include "../../Core/Uuid.h"
 
 #include <glog/logging.h>
 
@@ -111,13 +111,11 @@
   }
 
 
-  static bool ParseModifyRequest(DicomModification& target,
-                                 const RestApi::PostCall& call)
+
+  bool OrthancRestApi::ParseModifyRequest(DicomModification& target,
+                                          const Json::Value& request)
   {
-    // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"}}'
-
-    Json::Value request;
-    if (call.ParseJsonRequest(request) && request.isObject())
+    if (request.isObject())
     {
       if (request.isMember("RemovePrivateTags"))
       {
@@ -143,8 +141,25 @@
   }
 
 
+  static bool ParseModifyRequest(DicomModification& target,
+                                 const RestApiPostCall& call)
+  {
+    // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"}}'
+
+    Json::Value request;
+    if (call.ParseJsonRequest(request))
+    {
+      return OrthancRestApi::ParseModifyRequest(target, request);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
   static bool ParseAnonymizationRequest(DicomModification& target,
-                                        RestApi::PostCall& call)
+                                        RestApiPostCall& call)
   {
     // curl http://localhost:8042/instances/6e67da51-d119d6ae-c5667437-87b9a8a5-0f07c49f/anonymize -X POST -d '{"Replace":{"PatientName":"hello","0010-0020":"world"},"Keep":["StudyDescription", "SeriesDescription"],"KeepPrivateTags": null,"Remove":["Modality"]}' > Anonymized.dcm
 
@@ -174,11 +189,12 @@
         ParseListOfTags(target, request["Keep"], TagOperation_Keep);
       }
 
-      if (target.GetReplacement(DICOM_TAG_PATIENT_NAME) == patientName)
+      if (target.IsReplaced(DICOM_TAG_PATIENT_NAME) &&
+          target.GetReplacement(DICOM_TAG_PATIENT_NAME) == patientName)
       {
         // Overwrite the random Patient's Name by one that is more
         // user-friendly (provided none was specified by the user)
-        target.Replace(DICOM_TAG_PATIENT_NAME, GeneratePatientName(OrthancRestApi::GetContext(call)));
+        target.Replace(DICOM_TAG_PATIENT_NAME, GeneratePatientName(OrthancRestApi::GetContext(call)), true);
       }
 
       return true;
@@ -191,7 +207,7 @@
 
 
   static void AnonymizeOrModifyInstance(DicomModification& modification,
-                                        RestApi::PostCall& call)
+                                        RestApiPostCall& call)
   {
     std::string id = call.GetUriComponent("id", "");
 
@@ -207,7 +223,7 @@
                                         MetadataType metadataType,
                                         ChangeType changeType,
                                         ResourceType resourceType,
-                                        RestApi::PostCall& call)
+                                        RestApiPostCall& call)
   {
     bool isFirst = true;
     Json::Value result(Json::objectValue);
@@ -246,52 +262,61 @@
         continue;
       }
 
+
       ParsedDicomFile& original = locker->GetDicom();
       DicomInstanceHasher originalHasher = original.GetHasher();
 
 
       /**
-       * Compute the resulting DICOM instance and store it into the Orthanc store.
+       * Compute the resulting DICOM instance.
        **/
 
       std::auto_ptr<ParsedDicomFile> modified(original.Clone());
       modification.Apply(*modified);
 
-      std::string modifiedInstance;
-      if (context.Store(modifiedInstance, *modified) != StoreStatus_Success)
-      {
-        LOG(ERROR) << "Error while storing a modified instance " << *it;
-        return;
-      }
+      DicomInstanceToStore toStore;
+      toStore.SetParsedDicomFile(*modified);
 
 
       /**
-       * Record metadata information (AnonymizedFrom/ModifiedFrom).
+       * Prepare the metadata information to associate with the
+       * resulting DICOM instance (AnonymizedFrom/ModifiedFrom).
        **/
 
       DicomInstanceHasher modifiedHasher = modified->GetHasher();
 
       if (originalHasher.HashSeries() != modifiedHasher.HashSeries())
       {
-        context.GetIndex().SetMetadata(modifiedHasher.HashSeries(), 
-                                       metadataType, originalHasher.HashSeries());
+        toStore.AddMetadata(ResourceType_Series, metadataType, originalHasher.HashSeries());
       }
 
       if (originalHasher.HashStudy() != modifiedHasher.HashStudy())
       {
-        context.GetIndex().SetMetadata(modifiedHasher.HashStudy(), 
-                                       metadataType, originalHasher.HashStudy());
+        toStore.AddMetadata(ResourceType_Study, metadataType, originalHasher.HashStudy());
       }
 
       if (originalHasher.HashPatient() != modifiedHasher.HashPatient())
       {
-        context.GetIndex().SetMetadata(modifiedHasher.HashPatient(), 
-                                       metadataType, originalHasher.HashPatient());
+        toStore.AddMetadata(ResourceType_Patient, metadataType, originalHasher.HashPatient());
       }
 
       assert(*it == originalHasher.HashInstance());
+      toStore.AddMetadata(ResourceType_Instance, metadataType, *it);
+
+
+      /**
+       * Store the resulting DICOM instance into the Orthanc store.
+       **/
+
+      std::string modifiedInstance;
+      if (context.Store(modifiedInstance, toStore) != StoreStatus_Success)
+      {
+        LOG(ERROR) << "Error while storing a modified instance " << *it;
+        return;
+      }
+
+      // Sanity checks in debug mode
       assert(modifiedInstance == modifiedHasher.HashInstance());
-      context.GetIndex().SetMetadata(modifiedInstance, metadataType, *it);
 
 
       /**
@@ -333,9 +358,10 @@
 
 
 
-  static void ModifyInstance(RestApi::PostCall& call)
+  static void ModifyInstance(RestApiPostCall& call)
   {
     DicomModification modification;
+    modification.SetAllowManualIdentifiers(true);
 
     if (ParseModifyRequest(modification, call))
     {
@@ -361,9 +387,10 @@
   }
 
 
-  static void AnonymizeInstance(RestApi::PostCall& call)
+  static void AnonymizeInstance(RestApiPostCall& call)
   {
     DicomModification modification;
+    modification.SetAllowManualIdentifiers(true);
 
     if (ParseAnonymizationRequest(modification, call))
     {
@@ -374,7 +401,7 @@
 
   template <enum ChangeType changeType,
             enum ResourceType resourceType>
-  static void ModifyResource(RestApi::PostCall& call)
+  static void ModifyResource(RestApiPostCall& call)
   {
     DicomModification modification;
 
@@ -389,7 +416,7 @@
 
   template <enum ChangeType changeType,
             enum ResourceType resourceType>
-  static void AnonymizeResource(RestApi::PostCall& call)
+  static void AnonymizeResource(RestApiPostCall& call)
   {
     DicomModification modification;
 
@@ -401,29 +428,38 @@
   }
 
 
-  static void Create(RestApi::PostCall& call)
+  static void CreateDicom(RestApiPostCall& call)
   {
     // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World"}'
     // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World","PixelData":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="}'
 
-    Json::Value request;
-    if (call.ParseJsonRequest(request) && request.isObject())
+    Json::Value replacements;
+    if (call.ParseJsonRequest(replacements) && replacements.isObject())
     {
-      DicomModification modification;
-      ParseReplacements(modification, request);
-
       ParsedDicomFile dicom;
 
-      if (modification.IsReplaced(DICOM_TAG_PIXEL_DATA))
+      Json::Value::Members members = replacements.getMemberNames();
+      for (size_t i = 0; i < members.size(); i++)
       {
-        dicom.EmbedImage(modification.GetReplacement(DICOM_TAG_PIXEL_DATA));
-        modification.Keep(DICOM_TAG_PIXEL_DATA);
+        const std::string& name = members[i];
+        std::string value = replacements[name].asString();
+
+        DicomTag tag = FromDcmtkBridge::ParseTag(name);
+        if (tag == DICOM_TAG_PIXEL_DATA)
+        {
+          dicom.EmbedImage(value);
+        }
+        else
+        {
+          dicom.Replace(tag, value);
+        }
       }
 
-      modification.Apply(dicom);
+      DicomInstanceToStore toStore;
+      toStore.SetParsedDicomFile(dicom);
 
       std::string id;
-      StoreStatus status = OrthancRestApi::GetContext(call).Store(id, dicom);
+      StoreStatus status = OrthancRestApi::GetContext(call).Store(id, toStore);
 
       if (status == StoreStatus_Failure)
       {
@@ -448,6 +484,6 @@
     Register("/studies/{id}/anonymize", AnonymizeResource<ChangeType_ModifiedStudy, ResourceType_Study>);
     Register("/patients/{id}/anonymize", AnonymizeResource<ChangeType_ModifiedPatient, ResourceType_Patient>);
 
-    Register("/tools/create-dicom", Create);
+    Register("/tools/create-dicom", CreateDicom);
   }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -39,9 +39,9 @@
 
 namespace Orthanc
 {
-  void OrthancRestApi::AnswerStoredInstance(RestApi::PostCall& call,
+  void OrthancRestApi::AnswerStoredInstance(RestApiPostCall& call,
                                             const std::string& publicId,
-                                            StoreStatus status)
+                                            StoreStatus status) const
   {
     Json::Value result = Json::objectValue;
 
@@ -56,10 +56,19 @@
   }
 
 
+  void OrthancRestApi::ResetOrthanc(RestApiPostCall& call)
+  {
+    OrthancRestApi::GetApi(call).resetRequestReceived_ = true;
+    call.GetOutput().AnswerBuffer("{}", "application/json");
+  }
+
+
+
+
 
   // Upload of DICOM files through HTTP ---------------------------------------
 
-  static void UploadDicomFile(RestApi::PostCall& call)
+  static void UploadDicomFile(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -71,8 +80,11 @@
 
     LOG(INFO) << "Receiving a DICOM file of " << postData.size() << " bytes through HTTP";
 
+    DicomInstanceToStore toStore;
+    toStore.SetBuffer(postData);
+
     std::string publicId;
-    StoreStatus status = context.Store(publicId, postData);
+    StoreStatus status = context.Store(publicId, toStore);
 
     OrthancRestApi::GetApi(call).AnswerStoredInstance(call, publicId, status);
   }
@@ -82,7 +94,8 @@
   // Registration of the various REST handlers --------------------------------
 
   OrthancRestApi::OrthancRestApi(ServerContext& context) : 
-    context_(context)
+    context_(context),
+    resetRequestReceived_(false)
   {
     RegisterSystem();
 
@@ -93,5 +106,10 @@
     RegisterArchive();
 
     Register("/instances", UploadDicomFile);
+
+    // Auto-generated directories
+    Register("/tools", RestApi::AutoListChildren);
+    Register("/tools/reset", ResetOrthanc);
+    Register("/instances/{id}/frames/{frame}", RestApi::AutoListChildren);
   }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,8 +32,9 @@
 
 #pragma once
 
+#include "../../Core/RestApi/RestApi.h"
+#include "../DicomModification.h"
 #include "../ServerContext.h"
-#include "../../Core/RestApi/RestApi.h"
 
 #include <set>
 
@@ -46,6 +47,7 @@
 
   private:
     ServerContext& context_;
+    bool resetRequestReceived_;
 
     void RegisterSystem();
 
@@ -59,26 +61,36 @@
 
     void RegisterArchive();
 
+    static void ResetOrthanc(RestApiPostCall& call);
+
   public:
     OrthancRestApi(ServerContext& context);
 
-    static OrthancRestApi& GetApi(RestApi::Call& call)
+    const bool& ResetRequestReceivedFlag() const
+    {
+      return resetRequestReceived_;
+    }
+
+    static OrthancRestApi& GetApi(RestApiCall& call)
     {
       return dynamic_cast<OrthancRestApi&>(call.GetContext());
     }
 
-    static ServerContext& GetContext(RestApi::Call& call)
+    static ServerContext& GetContext(RestApiCall& call)
     {
       return GetApi(call).context_;
     }
 
-    static ServerIndex& GetIndex(RestApi::Call& call)
+    static ServerIndex& GetIndex(RestApiCall& call)
     {
       return GetContext(call).GetIndex();
     }
 
-    void AnswerStoredInstance(RestApi::PostCall& call,
+    void AnswerStoredInstance(RestApiPostCall& call,
                               const std::string& publicId,
-                              StoreStatus status);
+                              StoreStatus status) const;
+
+    static bool ParseModifyRequest(DicomModification& target,
+                                   const Json::Value& request);
   };
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -33,11 +33,13 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
+#include "../DicomDirWriter.h"
 #include "../../Core/Compression/HierarchicalZipWriter.h"
 #include "../../Core/HttpServer/FilesystemHttpSender.h"
 #include "../../Core/Uuid.h"
 
 #include <glog/logging.h>
+#include <stdio.h>
 
 #if defined(_MSC_VER)
 #define snprintf _snprintf
@@ -53,30 +55,45 @@
   static std::string GetDirectoryNameInArchive(const Json::Value& resource,
                                                ResourceType resourceType)
   {
+    std::string s;
+    const Json::Value& tags = resource["MainDicomTags"];
+
     switch (resourceType)
     {
       case ResourceType_Patient:
       {
-        std::string p = resource["MainDicomTags"]["PatientID"].asString();
-        std::string n = resource["MainDicomTags"]["PatientName"].asString();
-        return p + " " + n;
+        std::string p = tags["PatientID"].asString();
+        std::string n = tags["PatientName"].asString();
+        s = p + " " + n;
+        break;
       }
 
       case ResourceType_Study:
       {
-        return resource["MainDicomTags"]["StudyDescription"].asString();
+        std::string p;
+        if (tags.isMember("AccessionNumber"))
+        {
+          p = tags["AccessionNumber"].asString() + " ";
+        }
+
+        s = p + tags["StudyDescription"].asString();
+        break;
       }
         
       case ResourceType_Series:
       {
-        std::string d = resource["MainDicomTags"]["SeriesDescription"].asString();
-        std::string m = resource["MainDicomTags"]["Modality"].asString();
-        return m + " " + d;
+        std::string d = tags["SeriesDescription"].asString();
+        std::string m = tags["Modality"].asString();
+        s = m + " " + d;
+        break;
       }
         
       default:
         throw OrthancException(ErrorCode_InternalError);
     }
+
+    // Get rid of special characters
+    return Toolbox::ConvertToAscii(s);
   }
 
   static bool CreateRootDirectoryInArchive(HierarchicalZipWriter& writer,
@@ -125,13 +142,6 @@
                               const std::string& instancePublicId,
                               const char* filename)
   {
-    Json::Value instance;
-
-    if (!context.GetIndex().LookupResource(instance, instancePublicId, ResourceType_Instance))
-    {
-      return false;
-    }
-
     writer.OpenFile(filename);
 
     std::string dicom;
@@ -232,13 +242,10 @@
     return true;
   }                                 
 
-  template <enum ResourceType resourceType>
-  static void GetArchive(RestApi::GetCall& call)
+
+  static bool IsZip64Required(ServerIndex& index,
+                              const std::string& id)
   {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::string id = call.GetUriComponent("id", "");
-
     /**
      * Determine whether ZIP64 is required. Original ZIP format can
      * store up to 2GB of data (some implementation supporting up to
@@ -251,8 +258,8 @@
     unsigned int countStudies;
     unsigned int countSeries;
     unsigned int countInstances;
-    context.GetIndex().GetStatistics(compressedSize, uncompressedSize, 
-                                     countStudies, countSeries, countInstances, id);
+    index.GetStatistics(compressedSize, uncompressedSize, 
+                        countStudies, countSeries, countInstances, id);
     const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES ||
                           countInstances >= 65535);
 
@@ -260,6 +267,18 @@
               << (uncompressedSize / MEGA_BYTES) << "MB using the "
               << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
 
+    return isZip64;
+  }
+                              
+
+  template <enum ResourceType resourceType>
+  static void GetArchive(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string id = call.GetUriComponent("id", "");
+    bool isZip64 = IsZip64Required(context.GetIndex(), id);
+
     // Create a RAII for the temporary file to manage the ZIP file
     Toolbox::TemporaryFile tmp;
 
@@ -287,10 +306,75 @@
   }
 
 
+  static void GetMediaArchive(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string id = call.GetUriComponent("id", "");
+    bool isZip64 = IsZip64Required(context.GetIndex(), id);
+
+    // Create a RAII for the temporary file to manage the ZIP file
+    Toolbox::TemporaryFile tmp;
+
+    {
+      // Create a ZIP writer
+      HierarchicalZipWriter writer(tmp.GetPath().c_str());
+      writer.SetZip64(isZip64);
+      writer.OpenDirectory("IMAGES");
+
+      // Create the DICOMDIR writer
+      DicomDirWriter dicomDir;
+
+      // Retrieve the list of the instances
+      std::list<std::string> instances;
+      context.GetIndex().GetChildInstances(instances, id);
+
+      size_t pos = 0;
+      for (std::list<std::string>::const_iterator
+             it = instances.begin(); it != instances.end(); ++it, ++pos)
+      {
+        // "DICOM restricts the filenames on DICOM media to 8
+        // characters (some systems wrongly use 8.3, but this does not
+        // conform to the standard)."
+        std::string filename = "IM" + boost::lexical_cast<std::string>(pos);
+        writer.OpenFile(filename.c_str());
+
+        std::string dicom;
+        context.ReadFile(dicom, *it, FileContentType_Dicom);
+        writer.Write(dicom);
+
+        ParsedDicomFile parsed(dicom);
+        dicomDir.Add("IMAGES", filename, parsed);
+      }
+
+      // Add the DICOMDIR
+      writer.CloseDirectory();
+      writer.OpenFile("DICOMDIR");
+      std::string s;
+      dicomDir.Encode(s);
+      writer.Write(s);
+    }
+
+    // Prepare the sending of the ZIP file
+    FilesystemHttpSender sender(tmp.GetPath().c_str());
+    sender.SetContentType("application/zip");
+    sender.SetDownloadFilename(id + ".zip");
+
+    // Send the ZIP
+    call.GetOutput().AnswerFile(sender);
+
+    // The temporary file is automatically removed thanks to the RAII
+  }
+
+
   void OrthancRestApi::RegisterArchive()
   {
     Register("/patients/{id}/archive", GetArchive<ResourceType_Patient>);
     Register("/studies/{id}/archive", GetArchive<ResourceType_Study>);
     Register("/series/{id}/archive", GetArchive<ResourceType_Series>);
+
+    Register("/patients/{id}/media", GetMediaArchive);
+    Register("/studies/{id}/media", GetMediaArchive);
+    Register("/series/{id}/media", GetMediaArchive);
   }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -42,7 +42,7 @@
   static void GetSinceAndLimit(int64_t& since,
                                unsigned int& limit,
                                bool& last,
-                               const RestApi::GetCall& call)
+                               const RestApiGetCall& call)
   {
     static const unsigned int MAX_RESULTS = 100;
     
@@ -70,7 +70,7 @@
     }
   }
 
-  static void GetChanges(RestApi::GetCall& call)
+  static void GetChanges(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -81,15 +81,20 @@
     GetSinceAndLimit(since, limit, last, call);
 
     Json::Value result;
-    if ((!last && context.GetIndex().GetChanges(result, since, limit)) ||
-        ( last && context.GetIndex().GetLastChange(result)))
+    if (last)
     {
-      call.GetOutput().AnswerJson(result);
+      context.GetIndex().GetLastChange(result);
     }
+    else
+    {
+      context.GetIndex().GetChanges(result, since, limit);
+    }
+
+    call.GetOutput().AnswerJson(result);
   }
 
 
-  static void DeleteChanges(RestApi::DeleteCall& call)
+  static void DeleteChanges(RestApiDeleteCall& call)
   {
     OrthancRestApi::GetIndex(call).DeleteChanges();
     call.GetOutput().AnswerBuffer("", "text/plain");
@@ -98,7 +103,7 @@
 
   // Exports API --------------------------------------------------------------
  
-  static void GetExports(RestApi::GetCall& call)
+  static void GetExports(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -108,15 +113,20 @@
     GetSinceAndLimit(since, limit, last, call);
 
     Json::Value result;
-    if ((!last && context.GetIndex().GetExportedResources(result, since, limit)) ||
-        ( last && context.GetIndex().GetLastExportedResource(result)))
+    if (last)
     {
-      call.GetOutput().AnswerJson(result);
+      context.GetIndex().GetLastExportedResource(result);
     }
+    else
+    {
+      context.GetIndex().GetExportedResources(result, since, limit);
+    }
+
+    call.GetOutput().AnswerJson(result);
   }
 
 
-  static void DeleteExports(RestApi::DeleteCall& call)
+  static void DeleteExports(RestApiDeleteCall& call)
   {
     OrthancRestApi::GetIndex(call).DeleteExportedResources();
     call.GetOutput().AnswerBuffer("", "text/plain");
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -36,6 +36,9 @@
 #include "../OrthancInitialization.h"
 #include "../../Core/HttpClient.h"
 #include "../FromDcmtkBridge.h"
+#include "../Scheduler/ServerJob.h"
+#include "../Scheduler/StoreScuCommand.h"
+#include "../Scheduler/StorePeerCommand.h"
 
 #include <glog/logging.h>
 
@@ -65,7 +68,33 @@
     return true;
   }
 
-  static void DicomFindPatient(RestApi::PostCall& call)
+
+  static void DicomEcho(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote);
+
+    try
+    {
+      if (locker.GetConnection().Echo())
+      {
+        // Echo has succeeded
+        call.GetOutput().AnswerBuffer("{}", "application/json");
+        return;
+      }
+    }
+    catch (OrthancException&)
+    {
+    }
+
+    // Echo has failed
+    call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
+  }
+
+
+  static void DicomFindPatient(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -87,7 +116,7 @@
     call.GetOutput().AnswerJson(result);
   }
 
-  static void DicomFindStudy(RestApi::PostCall& call)
+  static void DicomFindStudy(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -115,7 +144,7 @@
     call.GetOutput().AnswerJson(result);
   }
 
-  static void DicomFindSeries(RestApi::PostCall& call)
+  static void DicomFindSeries(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -144,7 +173,7 @@
     call.GetOutput().AnswerJson(result);
   }
 
-  static void DicomFindInstance(RestApi::PostCall& call)
+  static void DicomFindInstance(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -174,7 +203,7 @@
     call.GetOutput().AnswerJson(result);
   }
 
-  static void DicomFind(RestApi::PostCall& call)
+  static void DicomFind(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -248,7 +277,7 @@
 
   static bool GetInstancesToExport(std::list<std::string>& instances,
                                    const std::string& remote,
-                                   RestApi::PostCall& call)
+                                   RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -268,7 +297,11 @@
 
     if (request.isString())
     {
-      context.GetIndex().LogExportedResource(request.asString(), remote);
+      if (Configuration::GetGlobalBoolParameter("LogExportedResources", true))
+      {
+        context.GetIndex().LogExportedResource(request.asString(), remote);
+      }
+
       context.GetIndex().GetChildInstances(instances, request.asString());
     }
     else if (request.isArray())
@@ -286,7 +319,10 @@
           return false;
         }
 
-        context.GetIndex().LogExportedResource(stripped, remote);
+        if (Configuration::GetGlobalBoolParameter("LogExportedResources", true))
+        {
+          context.GetIndex().LogExportedResource(stripped, remote);
+        }
        
         std::list<std::string> tmp;
         context.GetIndex().GetChildInstances(tmp, stripped);
@@ -308,7 +344,7 @@
   }
 
 
-  static void DicomStore(RestApi::PostCall& call)
+  static void DicomStore(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -321,19 +357,25 @@
     }
 
     RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote);
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), p);
 
+    ServerJob job;
     for (std::list<std::string>::const_iterator 
            it = instances.begin(); it != instances.end(); ++it)
     {
-      LOG(INFO) << "Sending resource " << *it << " to modality \"" << remote << "\"";
-
-      std::string dicom;
-      context.ReadFile(dicom, *it, FileContentType_Dicom);
-      locker.GetConnection().Store(dicom);
+      job.AddCommand(new StoreScuCommand(context, p, false)).AddInput(*it);
     }
 
-    call.GetOutput().AnswerBuffer("{}", "application/json");
+    job.SetDescription("HTTP request: Store-SCU to peer \"" + remote + "\"");
+
+    if (context.GetScheduler().SubmitAndWait(job))
+    {
+      // Success
+      call.GetOutput().AnswerBuffer("{}", "application/json");
+    }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
+    }
   }
 
 
@@ -345,7 +387,7 @@
     return peers.find(id) != peers.end();
   }
 
-  static void ListPeers(RestApi::GetCall& call)
+  static void ListPeers(RestApiGetCall& call)
   {
     OrthancRestApi::SetOfStrings peers;
     Configuration::GetListOfOrthancPeers(peers);
@@ -360,7 +402,7 @@
     call.GetOutput().AnswerJson(result);
   }
 
-  static void ListPeerOperations(RestApi::GetCall& call)
+  static void ListPeerOperations(RestApiGetCall& call)
   {
     OrthancRestApi::SetOfStrings peers;
     Configuration::GetListOfOrthancPeers(peers);
@@ -368,13 +410,11 @@
     std::string id = call.GetUriComponent("id", "");
     if (IsExistingPeer(peers, id))
     {
-      Json::Value result = Json::arrayValue;
-      result.append("store");
-      call.GetOutput().AnswerJson(result);
+      RestApi::AutoListChildren(call);
     }
   }
 
-  static void PeerStore(RestApi::PostCall& call)
+  static void PeerStore(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -389,35 +429,24 @@
     OrthancPeerParameters peer;
     Configuration::GetOrthancPeer(peer, remote);
 
-    // Configure the HTTP client
-    HttpClient client;
-    if (peer.GetUsername().size() != 0 && 
-        peer.GetPassword().size() != 0)
-    {
-      client.SetCredentials(peer.GetUsername().c_str(), 
-                            peer.GetPassword().c_str());
-    }
-
-    client.SetUrl(peer.GetUrl() + "instances");
-    client.SetMethod(HttpMethod_Post);
-
-    // Loop over the instances that are to be sent
+    ServerJob job;
     for (std::list<std::string>::const_iterator 
            it = instances.begin(); it != instances.end(); ++it)
     {
-      LOG(INFO) << "Sending resource " << *it << " to peer \"" << remote << "\"";
-
-      context.ReadFile(client.AccessPostData(), *it, FileContentType_Dicom);
-
-      std::string answer;
-      if (!client.Apply(answer))
-      {
-        LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << remote << "\"";
-        return;
-      }
+      job.AddCommand(new StorePeerCommand(context, peer, false)).AddInput(*it);
     }
 
-    call.GetOutput().AnswerBuffer("{}", "application/json");
+    job.SetDescription("HTTP request: POST to peer \"" + remote + "\"");
+
+    if (context.GetScheduler().SubmitAndWait(job))
+    {
+      // Success
+      call.GetOutput().AnswerBuffer("{}", "application/json");
+    }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
+    }
   }
 
 
@@ -429,7 +458,7 @@
     return modalities.find(id) != modalities.end();
   }
 
-  static void ListModalities(RestApi::GetCall& call)
+  static void ListModalities(RestApiGetCall& call)
   {
     OrthancRestApi::SetOfStrings modalities;
     Configuration::GetListOfDicomModalities(modalities);
@@ -445,7 +474,7 @@
   }
 
 
-  static void ListModalityOperations(RestApi::GetCall& call)
+  static void ListModalityOperations(RestApiGetCall& call)
   {
     OrthancRestApi::SetOfStrings modalities;
     Configuration::GetListOfDicomModalities(modalities);
@@ -453,19 +482,12 @@
     std::string id = call.GetUriComponent("id", "");
     if (IsExistingModality(modalities, id))
     {
-      Json::Value result = Json::arrayValue;
-      result.append("find-patient");
-      result.append("find-study");
-      result.append("find-series");
-      result.append("find-instance");
-      result.append("find");
-      result.append("store");
-      call.GetOutput().AnswerJson(result);
+      RestApi::AutoListChildren(call);
     }
   }
 
 
-  static void UpdateModality(RestApi::PutCall& call)
+  static void UpdateModality(RestApiPutCall& call)
   {
     Json::Value json;
     Json::Reader reader;
@@ -479,14 +501,14 @@
   }
 
 
-  static void DeleteModality(RestApi::DeleteCall& call)
+  static void DeleteModality(RestApiDeleteCall& call)
   {
     Configuration::RemoveModality(call.GetUriComponent("id", ""));
     call.GetOutput().AnswerBuffer("", "text/plain");
   }
 
 
-  static void UpdatePeer(RestApi::PutCall& call)
+  static void UpdatePeer(RestApiPutCall& call)
   {
     Json::Value json;
     Json::Reader reader;
@@ -500,7 +522,7 @@
   }
 
 
-  static void DeletePeer(RestApi::DeleteCall& call)
+  static void DeletePeer(RestApiDeleteCall& call)
   {
     Configuration::RemovePeer(call.GetUriComponent("id", ""));
     call.GetOutput().AnswerBuffer("", "text/plain");
@@ -513,6 +535,7 @@
     Register("/modalities/{id}", ListModalityOperations);
     Register("/modalities/{id}", UpdateModality);
     Register("/modalities/{id}", DeleteModality);
+    Register("/modalities/{id}/echo", DicomEcho);
     Register("/modalities/{id}/find-patient", DicomFindPatient);
     Register("/modalities/{id}/find-study", DicomFindStudy);
     Register("/modalities/{id}/find-series", DicomFindSeries);
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,6 +35,8 @@
 
 #include "../ServerToolbox.h"
 #include "../FromDcmtkBridge.h"
+#include "../ResourceFinder.h"
+#include "../DicomFindQuery.h"
 
 #include <glog/logging.h>
 
@@ -42,16 +44,48 @@
 {
   // List all the patients, studies, series or instances ----------------------
  
-  template <enum ResourceType resourceType>
-  static void ListResources(RestApi::GetCall& call)
+  static void AnswerListOfResources(RestApiOutput& output,
+                                    ServerIndex& index,
+                                    const std::list<std::string>& resources,
+                                    ResourceType level,
+                                    bool expand)
   {
-    Json::Value result;
-    OrthancRestApi::GetIndex(call).GetAllUuids(result, resourceType);
-    call.GetOutput().AnswerJson(result);
+    Json::Value answer = Json::arrayValue;
+
+    for (std::list<std::string>::const_iterator
+           resource = resources.begin(); resource != resources.end(); resource++)
+    {
+      if (expand)
+      {
+        Json::Value item;
+        if (index.LookupResource(item, *resource, level))
+        {
+          answer.append(item);
+        }
+      }
+      else
+      {
+        answer.append(*resource);
+      }
+    }
+
+    output.AnswerJson(answer);
+  }
+
+
+  template <enum ResourceType resourceType>
+  static void ListResources(RestApiGetCall& call)
+  {
+    ServerIndex& index = OrthancRestApi::GetIndex(call);
+
+    std::list<std::string> result;
+    index.GetAllUuids(result, resourceType);
+
+    AnswerListOfResources(call.GetOutput(), index, result, resourceType, call.HasArgument("expand"));
   }
 
   template <enum ResourceType resourceType>
-  static void GetSingleResource(RestApi::GetCall& call)
+  static void GetSingleResource(RestApiGetCall& call)
   {
     Json::Value result;
     if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType))
@@ -61,10 +95,10 @@
   }
 
   template <enum ResourceType resourceType>
-  static void DeleteSingleResource(RestApi::DeleteCall& call)
+  static void DeleteSingleResource(RestApiDeleteCall& call)
   {
     Json::Value result;
-    if (OrthancRestApi::GetIndex(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType))
+    if (OrthancRestApi::GetContext(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType))
     {
       call.GetOutput().AnswerJson(result);
     }
@@ -73,7 +107,7 @@
 
   // Get information about a single patient -----------------------------------
  
-  static void IsProtectedPatient(RestApi::GetCall& call)
+  static void IsProtectedPatient(RestApiGetCall& call)
   {
     std::string publicId = call.GetUriComponent("id", "");
     bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId);
@@ -81,7 +115,7 @@
   }
 
 
-  static void SetPatientProtection(RestApi::PutCall& call)
+  static void SetPatientProtection(RestApiPutCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -107,16 +141,16 @@
 
   // Get information about a single instance ----------------------------------
  
-  static void GetInstanceFile(RestApi::GetCall& call)
+  static void GetInstanceFile(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string publicId = call.GetUriComponent("id", "");
-    context.AnswerDicomFile(call.GetOutput(), publicId, FileContentType_Dicom);
+    context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom);
   }
 
 
-  static void ExportInstanceFile(RestApi::PostCall& call)
+  static void ExportInstanceFile(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -132,29 +166,44 @@
 
 
   template <bool simplify>
-  static void GetInstanceTags(RestApi::GetCall& call)
+  static void GetInstanceTags(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string publicId = call.GetUriComponent("id", "");
     
-    Json::Value full;
-    context.ReadJson(full, publicId);
-
     if (simplify)
     {
+      Json::Value full;
+      context.ReadJson(full, publicId);
+
       Json::Value simplified;
       SimplifyTags(simplified, full);
       call.GetOutput().AnswerJson(simplified);
     }
     else
     {
-      call.GetOutput().AnswerJson(full);
+      context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_DicomAsJson);
+    }
+  }
+
+
+  static void GetInstanceTagsBis(RestApiGetCall& call)
+  {
+    bool simplify = call.HasArgument("simplify");
+
+    if (simplify)
+    {
+      GetInstanceTags<true>(call);
+    }
+    else
+    {
+      GetInstanceTags<false>(call);
     }
   }
 
   
-  static void ListFrames(RestApi::GetCall& call)
+  static void ListFrames(RestApiGetCall& call)
   {
     Json::Value instance;
     if (OrthancRestApi::GetIndex(call).LookupResource(instance, call.GetUriComponent("id", ""), ResourceType_Instance))
@@ -182,7 +231,7 @@
 
 
   template <enum ImageExtractionMode mode>
-  static void GetImage(RestApi::GetCall& call)
+  static void GetImage(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -230,7 +279,7 @@
   }
 
 
-  static void GetMatlabImage(RestApi::GetCall& call)
+  static void GetMatlabImage(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
@@ -264,7 +313,7 @@
 
 
 
-  static void GetResourceStatistics(RestApi::GetCall& call)
+  static void GetResourceStatistics(RestApiGetCall& call)
   {
     std::string publicId = call.GetUriComponent("id", "");
     Json::Value result;
@@ -276,14 +325,14 @@
 
   // Handling of metadata -----------------------------------------------------
 
-  static void CheckValidResourceType(RestApi::Call& call)
+  static void CheckValidResourceType(RestApiCall& call)
   {
     std::string resourceType = call.GetUriComponent("resourceType", "");
     StringToResourceType(resourceType.c_str());
   }
 
 
-  static void ListMetadata(RestApi::GetCall& call)
+  static void ListMetadata(RestApiGetCall& call)
   {
     CheckValidResourceType(call);
     
@@ -303,7 +352,7 @@
   }
 
 
-  static void GetMetadata(RestApi::GetCall& call)
+  static void GetMetadata(RestApiGetCall& call)
   {
     CheckValidResourceType(call);
     
@@ -319,7 +368,7 @@
   }
 
 
-  static void DeleteMetadata(RestApi::DeleteCall& call)
+  static void DeleteMetadata(RestApiDeleteCall& call)
   {
     CheckValidResourceType(call);
 
@@ -337,7 +386,7 @@
   }
 
 
-  static void SetMetadata(RestApi::PutCall& call)
+  static void SetMetadata(RestApiPutCall& call)
   {
     CheckValidResourceType(call);
 
@@ -360,7 +409,7 @@
 
   // Handling of attached files -----------------------------------------------
 
-  static void ListAttachments(RestApi::GetCall& call)
+  static void ListAttachments(RestApiGetCall& call)
   {
     std::string resourceType = call.GetUriComponent("resourceType", "");
     std::string publicId = call.GetUriComponent("id", "");
@@ -379,7 +428,7 @@
   }
 
 
-  static bool GetAttachmentInfo(FileInfo& info, RestApi::Call& call)
+  static bool GetAttachmentInfo(FileInfo& info, RestApiCall& call)
   {
     CheckValidResourceType(call);
  
@@ -391,7 +440,7 @@
   }
 
 
-  static void GetAttachmentOperations(RestApi::GetCall& call)
+  static void GetAttachmentOperations(RestApiGetCall& call)
   {
     FileInfo info;
     if (GetAttachmentInfo(info, call))
@@ -427,24 +476,30 @@
 
   
   template <int uncompress>
-  static void GetAttachmentData(RestApi::GetCall& call)
+  static void GetAttachmentData(RestApiGetCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     CheckValidResourceType(call);
  
     std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
+    FileContentType type = StringToContentType(call.GetUriComponent("name", ""));
 
-    std::string content;
-    context.ReadFile(content, publicId, StringToContentType(name),
-                     (uncompress == 1));
-
-    call.GetOutput().AnswerBuffer(content, "application/octet-stream");
+    if (uncompress)
+    {
+      context.AnswerAttachment(call.GetOutput(), publicId, type);
+    }
+    else
+    {
+      // Return the raw data (possibly compressed), as stored on the filesystem
+      std::string content;
+      context.ReadFile(content, publicId, type, false);
+      call.GetOutput().AnswerBuffer(content, "application/octet-stream");
+    }
   }
 
 
-  static void GetAttachmentSize(RestApi::GetCall& call)
+  static void GetAttachmentSize(RestApiGetCall& call)
   {
     FileInfo info;
     if (GetAttachmentInfo(info, call))
@@ -454,7 +509,7 @@
   }
 
 
-  static void GetAttachmentCompressedSize(RestApi::GetCall& call)
+  static void GetAttachmentCompressedSize(RestApiGetCall& call)
   {
     FileInfo info;
     if (GetAttachmentInfo(info, call))
@@ -464,7 +519,7 @@
   }
 
 
-  static void GetAttachmentMD5(RestApi::GetCall& call)
+  static void GetAttachmentMD5(RestApiGetCall& call)
   {
     FileInfo info;
     if (GetAttachmentInfo(info, call) &&
@@ -475,7 +530,7 @@
   }
 
 
-  static void GetAttachmentCompressedMD5(RestApi::GetCall& call)
+  static void GetAttachmentCompressedMD5(RestApiGetCall& call)
   {
     FileInfo info;
     if (GetAttachmentInfo(info, call) &&
@@ -486,7 +541,7 @@
   }
 
 
-  static void VerifyAttachment(RestApi::PostCall& call)
+  static void VerifyAttachment(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
     CheckValidResourceType(call);
@@ -540,7 +595,7 @@
   }
 
 
-  static void UploadAttachment(RestApi::PutCall& call)
+  static void UploadAttachment(RestApiPutCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
     CheckValidResourceType(call);
@@ -560,7 +615,7 @@
   }
 
 
-  static void DeleteAttachment(RestApi::DeleteCall& call)
+  static void DeleteAttachment(RestApiDeleteCall& call)
   {
     CheckValidResourceType(call);
 
@@ -580,7 +635,7 @@
 
   // Raw access to the DICOM tags of an instance ------------------------------
 
-  static void GetRawContent(RestApi::GetCall& call)
+  static void GetRawContent(RestApiGetCall& call)
   {
     std::string id = call.GetUriComponent("id", "");
 
@@ -591,6 +646,394 @@
 
 
 
+  static bool ExtractSharedTags(Json::Value& shared,
+                                ServerContext& context,
+                                const std::string& publicId)
+  {
+    // Retrieve all the instances of this patient/study/series
+    typedef std::list<std::string> Instances;
+    Instances instances;
+    context.GetIndex().GetChildInstances(instances, publicId);  // (*)
+
+    // Loop over the instances
+    bool isFirst = true;
+    shared = Json::objectValue;
+
+    for (Instances::const_iterator it = instances.begin();
+         it != instances.end(); ++it)
+    {
+      // Get the tags of the current instance, in the simplified format
+      Json::Value tags;
+
+      try
+      {
+        context.ReadJson(tags, *it);
+      }
+      catch (OrthancException&)
+      {
+        // Race condition: This instance has been removed since
+        // (*). Ignore this instance.
+        continue;
+      }
+
+      if (tags.type() != Json::objectValue)
+      {
+        return false;   // Error
+      }
+
+      // Only keep the tags that are mapped to a string
+      Json::Value::Members members = tags.getMemberNames();
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        const Json::Value& tag = tags[members[i]];
+        if (tag.type() != Json::objectValue ||
+            tag["Type"].type() != Json::stringValue ||
+            tag["Type"].asString() != "String")
+        {
+          tags.removeMember(members[i]);
+        }
+      }
+
+      if (isFirst)
+      {
+        // This is the first instance, keep its tags as such
+        shared = tags;
+        isFirst = false;
+      }
+      else
+      {
+        // Loop over all the members of the shared tags extracted so
+        // far. If the value of one of these tags does not match its
+        // value in the current instance, remove it.
+        members = shared.getMemberNames();
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          if (!tags.isMember(members[i]) ||
+              tags[members[i]]["Value"].asString() != shared[members[i]]["Value"].asString())
+          {
+            shared.removeMember(members[i]);
+          }
+        }
+      }
+    }
+
+    return true;
+  }
+
+
+  static void GetSharedTags(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    std::string publicId = call.GetUriComponent("id", "");
+    bool simplify = call.HasArgument("simplify");
+
+    Json::Value sharedTags;
+    if (ExtractSharedTags(sharedTags, context, publicId))
+    {
+      // Success: Send the value of the shared tags
+      if (simplify)
+      {
+        Json::Value simplified;
+        SimplifyTags(simplified, sharedTags);
+        call.GetOutput().AnswerJson(simplified);
+      }
+      else
+      {
+        call.GetOutput().AnswerJson(sharedTags);
+      }
+    }
+  }
+
+
+  static void GetModuleInternal(RestApiGetCall& call,
+                                ResourceType resourceType,
+                                DicomModule module)
+  {
+    if (!((resourceType == ResourceType_Patient && module == DicomModule_Patient) ||
+          (resourceType == ResourceType_Study && module == DicomModule_Patient) ||
+          (resourceType == ResourceType_Study && module == DicomModule_Study) ||
+          (resourceType == ResourceType_Series && module == DicomModule_Series) ||
+          (resourceType == ResourceType_Instance && module == DicomModule_Instance) ||
+          (resourceType == ResourceType_Instance && module == DicomModule_Image)))
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    std::string publicId = call.GetUriComponent("id", "");
+    bool simplify = call.HasArgument("simplify");
+
+    typedef std::set<DicomTag> ModuleTags;
+    ModuleTags moduleTags;
+    DicomTag::GetTagsForModule(moduleTags, module);
+
+    Json::Value tags;
+
+    if (resourceType != ResourceType_Instance)
+    {
+      // Retrieve all the instances of this patient/study/series
+      typedef std::list<std::string> Instances;
+      Instances instances;
+      context.GetIndex().GetChildInstances(instances, publicId);
+
+      if (instances.empty())
+      {
+        return;   // Error: No instance (should never happen)
+      }
+
+      // Select one child instance
+      publicId = instances.front();
+    }
+
+    context.ReadJson(tags, publicId);
+    
+    // Filter the tags of the instance according to the module
+    Json::Value result = Json::objectValue;
+    for (ModuleTags::const_iterator tag = moduleTags.begin(); tag != moduleTags.end(); ++tag)
+    {
+      std::string s = tag->Format();
+      if (tags.isMember(s))
+      {
+        result[s] = tags[s];
+      }      
+    }
+
+    if (simplify)
+    {
+      Json::Value simplified;
+      SimplifyTags(simplified, result);
+      call.GetOutput().AnswerJson(simplified);
+    }
+    else
+    {
+      call.GetOutput().AnswerJson(result);
+    }    
+  }
+    
+
+
+  template <enum ResourceType resourceType, 
+            enum DicomModule module>
+  static void GetModule(RestApiGetCall& call)
+  {
+    GetModuleInternal(call, resourceType, module);
+  }
+
+
+  static void Lookup(RestApiPostCall& call)
+  {
+    typedef std::list< std::pair<ResourceType, std::string> >  Resources;
+
+    std::string tag = call.GetPostBody();
+    Resources resources;
+
+    OrthancRestApi::GetIndex(call).LookupIdentifier(resources, tag);
+
+    Json::Value result = Json::arrayValue;
+    
+    for (Resources::const_iterator it = resources.begin();
+         it != resources.end(); ++it)
+    {     
+      ResourceType type = it->first;
+      const std::string& id = it->second;
+      
+      Json::Value item = Json::objectValue;
+      item["Type"] = EnumerationToString(type);
+      item["ID"] = id;
+      item["Path"] = GetBasePath(type, id);
+    
+      result.append(item);
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  static void Find(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value request;
+    if (call.ParseJsonRequest(request) &&
+        request.type() == Json::objectValue &&
+        request.isMember("Level") &&
+        request.isMember("Query") &&
+        request["Level"].type() == Json::stringValue &&
+        request["Query"].type() == Json::objectValue)
+    {
+      bool expand = false;
+      if (request.isMember("Expand"))
+      {
+        expand = request["Expand"].asBool();
+      }
+
+      std::string level = request["Level"].asString();
+
+      DicomFindQuery query;
+      query.SetLevel(StringToResourceType(level.c_str()));
+
+      Json::Value::Members members = request["Query"].getMemberNames();
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        if (request["Query"][members[i]].type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadRequest);
+        }
+
+        query.SetConstraint(FromDcmtkBridge::ParseTag(members[i]), 
+                            request["Query"][members[i]].asString());
+      }
+      
+      std::list<std::string> resources;
+      ResourceFinder finder(context);
+      finder.Apply(resources, query);
+      AnswerListOfResources(call.GetOutput(), context.GetIndex(), resources, query.GetLevel(), expand);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+  }
+
+
+  template <enum ResourceType start, 
+            enum ResourceType end>
+  static void GetChildResources(RestApiGetCall& call)
+  {
+    ServerIndex& index = OrthancRestApi::GetIndex(call);
+
+    std::list<std::string> a, b, c;
+    a.push_back(call.GetUriComponent("id", ""));
+
+    ResourceType type = start;
+    while (type != end)
+    {
+      b.clear();
+
+      for (std::list<std::string>::const_iterator
+             it = a.begin(); it != a.end(); ++it)
+      {
+        index.GetChildren(c, *it);
+        b.splice(b.begin(), c);
+      }
+
+      switch (type)
+      {
+        case ResourceType_Patient:
+          type = ResourceType_Study;
+          break;
+
+        case ResourceType_Study:
+          type = ResourceType_Series;
+          break;
+
+        case ResourceType_Series:
+          type = ResourceType_Instance;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      a.clear();
+      a.splice(a.begin(), b);
+    }
+
+    Json::Value result = Json::arrayValue;
+
+    for (std::list<std::string>::const_iterator
+           it = a.begin(); it != a.end(); ++it)
+    {
+      Json::Value item;
+
+      if (OrthancRestApi::GetIndex(call).LookupResource(item, *it, end))
+      {
+        result.append(item);
+      }
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  static void GetChildInstancesTags(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    std::string publicId = call.GetUriComponent("id", "");
+    bool simplify = call.HasArgument("simplify");
+
+    // Retrieve all the instances of this patient/study/series
+    typedef std::list<std::string> Instances;
+    Instances instances;
+
+    context.GetIndex().GetChildInstances(instances, publicId);  // (*)
+
+    Json::Value result = Json::objectValue;
+
+    for (Instances::const_iterator it = instances.begin();
+         it != instances.end(); ++it)
+    {
+      Json::Value full;
+      context.ReadJson(full, *it);
+
+      if (simplify)
+      {
+        Json::Value simplified;
+        SimplifyTags(simplified, full);
+        result[*it] = simplified;
+      }
+      else
+      {
+        result[*it] = full;
+      }
+    }
+    
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+
+  template <enum ResourceType start, 
+            enum ResourceType end>
+  static void GetParentResource(RestApiGetCall& call)
+  {
+    assert(start > end);
+
+    ServerIndex& index = OrthancRestApi::GetIndex(call);
+    
+    std::string current = call.GetUriComponent("id", "");
+    ResourceType currentType = start;
+    while (currentType > end)
+    {
+      std::string parent;
+      if (!index.LookupParent(parent, current))
+      {
+        // Error that could happen if the resource gets deleted by
+        // another concurrent call
+        return;
+      }
+      
+      current = parent;
+      switch (currentType)
+      {
+        case ResourceType_Instance:  currentType = ResourceType_Series; break;
+        case ResourceType_Series:    currentType = ResourceType_Study; break;
+        case ResourceType_Study:     currentType = ResourceType_Patient; break;
+        default:                     throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    assert(currentType == end);
+
+    Json::Value result;
+    if (index.LookupResource(result, current, end))
+    {
+      call.GetOutput().AnswerJson(result);
+    }
+  }
+
+
+
   void OrthancRestApi::RegisterResources()
   {
     Register("/instances", ListResources<ResourceType_Instance>);
@@ -612,9 +1055,19 @@
     Register("/studies/{id}/statistics", GetResourceStatistics);
     Register("/series/{id}/statistics", GetResourceStatistics);
 
+    Register("/patients/{id}/shared-tags", GetSharedTags);
+    Register("/series/{id}/shared-tags", GetSharedTags);
+    Register("/studies/{id}/shared-tags", GetSharedTags);
+
+    Register("/instances/{id}/module", GetModule<ResourceType_Instance, DicomModule_Instance>);
+    Register("/patients/{id}/module", GetModule<ResourceType_Patient, DicomModule_Patient>);
+    Register("/series/{id}/module", GetModule<ResourceType_Series, DicomModule_Series>);
+    Register("/studies/{id}/module", GetModule<ResourceType_Study, DicomModule_Study>);
+    Register("/studies/{id}/module-patient", GetModule<ResourceType_Study, DicomModule_Patient>);
+
     Register("/instances/{id}/file", GetInstanceFile);
     Register("/instances/{id}/export", ExportInstanceFile);
-    Register("/instances/{id}/tags", GetInstanceTags<false>);
+    Register("/instances/{id}/tags", GetInstanceTagsBis);
     Register("/instances/{id}/simplified-tags", GetInstanceTags<true>);
     Register("/instances/{id}/frames", ListFrames);
 
@@ -649,6 +1102,27 @@
     Register("/{resourceType}/{id}/attachments/{name}/verify-md5", VerifyAttachment);
     Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment);
 
+    Register("/tools/lookup", Lookup);
+    Register("/tools/find", Find);
+
+    Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>);
+    Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>);
+    Register("/patients/{id}/instances", GetChildResources<ResourceType_Patient, ResourceType_Instance>);
+    Register("/studies/{id}/series", GetChildResources<ResourceType_Study, ResourceType_Series>);
+    Register("/studies/{id}/instances", GetChildResources<ResourceType_Study, ResourceType_Instance>);
+    Register("/series/{id}/instances", GetChildResources<ResourceType_Series, ResourceType_Instance>);
+
+    Register("/studies/{id}/patient", GetParentResource<ResourceType_Study, ResourceType_Patient>);
+    Register("/series/{id}/patient", GetParentResource<ResourceType_Series, ResourceType_Patient>);
+    Register("/series/{id}/study", GetParentResource<ResourceType_Series, ResourceType_Study>);
+    Register("/instances/{id}/patient", GetParentResource<ResourceType_Instance, ResourceType_Patient>);
+    Register("/instances/{id}/study", GetParentResource<ResourceType_Instance, ResourceType_Study>);
+    Register("/instances/{id}/series", GetParentResource<ResourceType_Instance, ResourceType_Series>);
+
+    Register("/patients/{id}/instances-tags", GetChildInstancesTags);
+    Register("/studies/{id}/instances-tags", GetChildInstancesTags);
+    Register("/series/{id}/instances-tags", GetChildInstancesTags);
+
     Register("/instances/{id}/content/*", GetRawContent);
   }
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -35,6 +35,8 @@
 
 #include "../OrthancInitialization.h"
 #include "../FromDcmtkBridge.h"
+#include "../../Plugins/Engine/PluginsManager.h"
+#include "../../Plugins/Engine/OrthancPlugins.h"
 
 #include <glog/logging.h>
 
@@ -43,29 +45,30 @@
 {
   // System information -------------------------------------------------------
 
-  static void ServeRoot(RestApi::GetCall& call)
+  static void ServeRoot(RestApiGetCall& call)
   {
     call.GetOutput().Redirect("app/explorer.html");
   }
  
-  static void GetSystemInformation(RestApi::GetCall& call)
+  static void GetSystemInformation(RestApiGetCall& call)
   {
     Json::Value result = Json::objectValue;
 
     result["Version"] = ORTHANC_VERSION;
     result["Name"] = Configuration::GetGlobalStringParameter("Name", "");
+    result["DatabaseVersion"] = boost::lexical_cast<int>(OrthancRestApi::GetIndex(call).GetGlobalProperty(GlobalProperty_DatabaseSchemaVersion, "0"));
 
     call.GetOutput().AnswerJson(result);
   }
 
-  static void GetStatistics(RestApi::GetCall& call)
+  static void GetStatistics(RestApiGetCall& call)
   {
     Json::Value result = Json::objectValue;
     OrthancRestApi::GetIndex(call).ComputeStatistics(result);
     call.GetOutput().AnswerJson(result);
   }
 
-  static void GenerateUid(RestApi::GetCall& call)
+  static void GenerateUid(RestApiGetCall& call)
   {
     std::string level = call.GetArgument("level", "");
     if (level == "patient")
@@ -86,19 +89,122 @@
     }
   }
 
-  static void ExecuteScript(RestApi::PostCall& call)
+  static void ExecuteScript(RestApiPostCall& call)
   {
     std::string result;
     ServerContext& context = OrthancRestApi::GetContext(call);
-    context.GetLuaContext().Execute(result, call.GetPostBody());
+
+    {
+      ServerContext::LuaContextLocker locker(context);
+      locker.GetLua().Execute(result, call.GetPostBody());
+    }
+
     call.GetOutput().AnswerBuffer(result, "text/plain");
   }
 
-  static void GetNowIsoString(RestApi::GetCall& call)
+  static void GetNowIsoString(RestApiGetCall& call)
   {
     call.GetOutput().AnswerBuffer(Toolbox::GetNowIsoString(), "text/plain");
   }
 
+
+  static void GetDicomConformanceStatement(RestApiGetCall& call)
+  {
+    std::string statement;
+    GetFileResource(statement, EmbeddedResources::DICOM_CONFORMANCE_STATEMENT);
+    call.GetOutput().AnswerBuffer(statement, "text/plain");
+  }
+
+
+  // Plugins information ------------------------------------------------------
+
+  static void ListPlugins(RestApiGetCall& call)
+  {
+    Json::Value v = Json::arrayValue;
+
+    v.append("explorer.js");
+
+    if (OrthancRestApi::GetContext(call).HasPlugins())
+    {
+      std::list<std::string> plugins;
+      OrthancRestApi::GetContext(call).GetPluginsManager().ListPlugins(plugins);
+
+      for (std::list<std::string>::const_iterator 
+             it = plugins.begin(); it != plugins.end(); ++it)
+      {
+        v.append(*it);
+      }
+    }
+
+    call.GetOutput().AnswerJson(v);
+  }
+
+
+  static void GetPlugin(RestApiGetCall& call)
+  {
+    if (!OrthancRestApi::GetContext(call).HasPlugins())
+    {
+      return;
+    }
+
+    const PluginsManager& manager = OrthancRestApi::GetContext(call).GetPluginsManager();
+    std::string id = call.GetUriComponent("id", "");
+
+    if (manager.HasPlugin(id))
+    {
+      Json::Value v = Json::objectValue;
+      v["ID"] = id;
+      v["Version"] = manager.GetPluginVersion(id);
+
+      const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetOrthancPlugins();
+      const char *c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_RootUri);
+      if (c != NULL)
+      {
+        v["RootUri"] = c;
+      }
+
+      c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_Description);
+      if (c != NULL)
+      {
+        v["Description"] = c;
+      }
+
+      c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_OrthancExplorer);
+      v["ExtendsOrthancExplorer"] = (c != NULL);
+
+      call.GetOutput().AnswerJson(v);
+    }
+  }
+
+
+  static void GetOrthancExplorerPlugins(RestApiGetCall& call)
+  {
+    std::string s = "// Extensions to Orthanc Explorer by the registered plugins\n\n";
+
+    if (OrthancRestApi::GetContext(call).HasPlugins())
+    {
+      const PluginsManager& manager = OrthancRestApi::GetContext(call).GetPluginsManager();
+      const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetOrthancPlugins();
+
+      std::list<std::string> lst;
+      OrthancRestApi::GetContext(call).GetPluginsManager().ListPlugins(lst);
+
+      for (std::list<std::string>::const_iterator
+             it = lst.begin(); it != lst.end(); ++it)
+      {
+        const char* tmp = plugins.GetProperty(it->c_str(), _OrthancPluginProperty_OrthancExplorer);
+        if (tmp != NULL)
+        {
+          s += "/**\n * From plugin: " + *it + " (version " + manager.GetPluginVersion(*it) + ")\n **/\n\n";
+          s += std::string(tmp) + "\n\n";
+        }
+      }
+    }
+
+    call.GetOutput().AnswerBuffer(s, "application/javascript");
+  }
+
+
   void OrthancRestApi::RegisterSystem()
   {
     Register("/", ServeRoot);
@@ -107,5 +213,10 @@
     Register("/tools/generate-uid", GenerateUid);
     Register("/tools/execute-script", ExecuteScript);
     Register("/tools/now", GetNowIsoString);
+    Register("/tools/dicom-conformance", GetDicomConformanceStatement);
+
+    Register("/plugins", ListPlugins);
+    Register("/plugins/{id}", GetPlugin);
+    Register("/plugins/explorer.js", GetOrthancExplorerPlugins);
   }
 }
--- a/OrthancServer/ParsedDicomFile.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ParsedDicomFile.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -80,6 +80,7 @@
 
 #include "ParsedDicomFile.h"
 
+#include "ServerToolbox.h"
 #include "FromDcmtkBridge.h"
 #include "ToDcmtkBridge.h"
 #include "Internals/DicomImageDecoder.h"
@@ -146,6 +147,7 @@
   struct ParsedDicomFile::PImpl
   {
     std::auto_ptr<DcmFileFormat> file_;
+    Encoding encoding_;
   };
 
 
@@ -170,6 +172,8 @@
     }
     pimpl_->file_->loadAllDataIntoMemory();
     pimpl_->file_->transferEnd();
+
+    pimpl_->encoding_ = FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset());
   }
 
 
@@ -261,7 +265,8 @@
     Uint32 length = element.getLength(transferSyntax);
     Uint32 offset = 0;
 
-    output.GetLowLevelOutput().SendOkHeader(CONTENT_TYPE_OCTET_STREAM, true, length, NULL);
+    output.GetLowLevelOutput().SetContentType(CONTENT_TYPE_OCTET_STREAM);
+    output.GetLowLevelOutput().SetContentLength(length);
 
     while (offset < length)
     {
@@ -279,7 +284,7 @@
 
       if (cond.good())
       {
-        output.GetLowLevelOutput().Send(&buffer[0], nbytes);
+        output.GetLowLevelOutput().SendBody(&buffer[0], nbytes);
         offset += nbytes;
       }
       else
@@ -753,24 +758,42 @@
 
 
 
-  void ParsedDicomFile::RemovePrivateTags()
+  void ParsedDicomFile::RemovePrivateTagsInternal(const std::set<DicomTag>* toKeep)
   {
+    DcmDataset& dataset = *pimpl_->file_->getDataset();
+
+    // Loop over the dataset to detect its private tags
     typedef std::list<DcmElement*> Tags;
-
     Tags privateTags;
 
-    DcmDataset& dataset = *pimpl_->file_->getDataset();
     for (unsigned long i = 0; i < dataset.card(); i++)
     {
       DcmElement* element = dataset.getElement(i);
       DcmTag tag(element->getTag());
-      if (!strcmp("PrivateCreator", tag.getTagName()) ||  // TODO - This may change with future versions of DCMTK
-          tag.getPrivateCreator() != NULL)
+
+      // Is this a private tag?
+      if (FromDcmtkBridge::IsPrivateTag(tag))
       {
-        privateTags.push_back(element);
+        bool remove = true;
+
+        // Check whether this private tag is to be kept
+        if (toKeep != NULL)
+        {
+          DicomTag tmp = FromDcmtkBridge::Convert(tag);
+          if (toKeep->find(tmp) != toKeep->end())
+          {
+            remove = false;  // Keep it
+          }
+        }
+            
+        if (remove)
+        {
+          privateTags.push_back(element);
+        }
       }
     }
 
+    // Loop over the detected private tags to remove them
     for (Tags::iterator it = privateTags.begin(); 
          it != privateTags.end(); ++it)
     {
@@ -784,13 +807,30 @@
 
 
 
+
   void ParsedDicomFile::Insert(const DicomTag& tag,
                                const std::string& value)
   {
-    std::auto_ptr<DcmElement> element(CreateElementForTag(tag));
-    FillElementWithString(*element, tag, value);
+    OFCondition cond;
+
+    if (FromDcmtkBridge::IsPrivateTag(tag))
+    {
+      // This is a private tag
+      // http://support.dcmtk.org/redmine/projects/dcmtk/wiki/howto_addprivatedata
 
-    if (!pimpl_->file_->getDataset()->insert(element.release(), false, false).good())
+      DcmTag key(tag.GetGroup(), tag.GetElement(), EVR_OB);
+      cond = pimpl_->file_->getDataset()->putAndInsertUint8Array
+        (key, (const Uint8*) value.c_str(), value.size(), false);
+    }
+    else
+    {
+      std::auto_ptr<DcmElement> element(CreateElementForTag(tag));
+      FillElementWithString(*element, tag, value);
+
+      cond = pimpl_->file_->getDataset()->insert(element.release(), false, false);
+    }
+
+    if (!cond.good())
     {
       // This field already exists
       throw OrthancException(ErrorCode_InternalError);
@@ -824,7 +864,17 @@
     }
     else
     {
-      FillElementWithString(*element, tag, value);
+      if (FromDcmtkBridge::IsPrivateTag(tag))
+      {
+        if (!element->putUint8Array((const Uint8*) value.c_str(), value.size()).good())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+      else
+      {
+        FillElementWithString(*element, tag, value);
+      }
     }
 
 
@@ -852,7 +902,7 @@
   void ParsedDicomFile::Answer(RestApiOutput& output)
   {
     std::string serialized;
-    if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, pimpl_->file_->getDataset()))
+    if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset()))
     {
       output.AnswerBuffer(serialized, CONTENT_TYPE_OCTET_STREAM);
     }
@@ -865,29 +915,56 @@
   {
     DcmTagKey k(tag.GetGroup(), tag.GetElement());
     DcmDataset& dataset = *pimpl_->file_->getDataset();
-    DcmElement* element = NULL;
-    if (!dataset.findAndGetElement(k, element).good() ||
-        element == NULL)
+
+    if (FromDcmtkBridge::IsPrivateTag(tag))
     {
-      return false;
-    }
+      const Uint8* data = NULL;   // This is freed in the destructor of the dataset
+      long unsigned int count = 0;
 
-    std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(*element));
+      if (dataset.findAndGetUint8Array(k, data, &count).good())
+      {
+        if (count > 0)
+        {
+          assert(data != NULL);
+          value.assign(reinterpret_cast<const char*>(data), count);
+        }
+        else
+        {
+          value.clear();
+        }
 
-    if (v.get() == NULL)
-    {
-      value = "";
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
     else
     {
-      value = v->AsString();
-    }
+      DcmElement* element = NULL;
+      if (!dataset.findAndGetElement(k, element).good() ||
+          element == NULL)
+      {
+        return false;
+      }
+
+      std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(*element, pimpl_->encoding_));
 
-    return true;
+      if (v.get() == NULL)
+      {
+        value = "";
+      }
+      else
+      {
+        value = v->AsString();
+      }
+
+      return true;
+    }
   }
 
 
-
   DicomInstanceHasher ParsedDicomFile::GetHasher()
   {
     std::string patientId, studyUid, seriesUid, instanceUid;
@@ -904,98 +981,6 @@
   }
 
 
-  static void StoreElement(Json::Value& target,
-                           DcmElement& element,
-                           unsigned int maxStringLength);
-
-  static void StoreItem(Json::Value& target,
-                        DcmItem& item,
-                        unsigned int maxStringLength)
-  {
-    target = Json::Value(Json::objectValue);
-
-    for (unsigned long i = 0; i < item.card(); i++)
-    {
-      DcmElement* element = item.getElement(i);
-      StoreElement(target, *element, maxStringLength);
-    }
-  }
-
-
-  static void StoreElement(Json::Value& target,
-                           DcmElement& element,
-                           unsigned int maxStringLength)
-  {
-    assert(target.type() == Json::objectValue);
-
-    DicomTag tag(FromDcmtkBridge::GetTag(element));
-    const std::string formattedTag = tag.Format();
-
-#if 0
-    const std::string tagName = FromDcmtkBridge::GetName(tag);
-#else
-    // This version of the code gives access to the name of the private tags
-    DcmTag tagbis(element.getTag());
-    const std::string tagName(tagbis.getTagName());      
-#endif
-
-    if (element.isLeaf())
-    {
-      Json::Value value(Json::objectValue);
-      value["Name"] = tagName;
-
-      if (tagbis.getPrivateCreator() != NULL)
-      {
-        value["PrivateCreator"] = tagbis.getPrivateCreator();
-      }
-
-      std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(element));
-      if (v->IsNull())
-      {
-        value["Type"] = "Null";
-        value["Value"] = Json::nullValue;
-      }
-      else
-      {
-        std::string s = v->AsString();
-        if (maxStringLength == 0 ||
-            s.size() <= maxStringLength)
-        {
-          value["Type"] = "String";
-          value["Value"] = s;
-        }
-        else
-        {
-          value["Type"] = "TooLong";
-          value["Value"] = Json::nullValue;
-        }
-      }
-
-      target[formattedTag] = value;
-    }
-    else
-    {
-      Json::Value children(Json::arrayValue);
-
-      // "All subclasses of DcmElement except for DcmSequenceOfItems
-      // are leaf nodes, while DcmSequenceOfItems, DcmItem, DcmDataset
-      // etc. are not." The following cast is thus OK.
-      DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(element);
-
-      for (unsigned long i = 0; i < sequence.card(); i++)
-      {
-        DcmItem* child = sequence.getItem(i);
-        Json::Value& v = children.append(Json::objectValue);
-        StoreItem(v, *child, maxStringLength);
-      }  
-
-      target[formattedTag]["Name"] = tagName;
-      target[formattedTag]["Type"] = "Sequence";
-      target[formattedTag]["Value"] = children;
-    }
-  }
-
-
   template <typename T>
   static void ExtractPngImageTruncate(std::string& result,
                                       DicomIntegerPixelAccessor& accessor,
@@ -1028,7 +1013,7 @@
 
   void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer)
   {
-    FromDcmtkBridge::SaveToMemoryBuffer(buffer, pimpl_->file_->getDataset());
+    FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset());
   }
 
 
@@ -1044,6 +1029,7 @@
   ParsedDicomFile::ParsedDicomFile() : pimpl_(new PImpl)
   {
     pimpl_->file_.reset(new DcmFileFormat);
+    pimpl_->encoding_ = Encoding_Ascii;
     Replace(DICOM_TAG_PATIENT_ID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient));
     Replace(DICOM_TAG_STUDY_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study));
     Replace(DICOM_TAG_SERIES_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series));
@@ -1073,6 +1059,8 @@
     pimpl_(new PImpl)
   {
     pimpl_->file_.reset(dynamic_cast<DcmFileFormat*>(other.pimpl_->file_->clone()));
+
+    pimpl_->encoding_ = other.pimpl_->encoding_;
   }
 
 
@@ -1241,15 +1229,15 @@
     switch (mode)
     {
       case ImageExtractionMode_UInt8:
-        ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale8);
+        ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale8, false);
         break;
 
       case ImageExtractionMode_UInt16:
-        ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale16);
+        ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale16, false);
         break;
 
       case ImageExtractionMode_Int16:
-        ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_SignedGrayscale16);
+        ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_SignedGrayscale16, false);
         break;
 
       case ImageExtractionMode_Preview:
@@ -1279,4 +1267,96 @@
     writer.WriteToMemory(result, accessor);
   }
 
+
+  Encoding ParsedDicomFile::GetEncoding() const
+  {
+    return pimpl_->encoding_;
+  }
+
+
+  void ParsedDicomFile::SetEncoding(Encoding encoding)
+  {
+    std::string s;
+
+    // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/
+    switch (encoding)
+    {
+      case Encoding_Utf8:
+      case Encoding_Ascii:
+        s = "ISO_IR 192";
+        break;
+
+      case Encoding_Windows1251:
+        // This Cyrillic codepage is not officially supported by the
+        // DICOM standard. Do not set the SpecificCharacterSet tag.
+        return;
+
+      case Encoding_Latin1:
+        s = "ISO_IR 100";
+        break;
+
+      case Encoding_Latin2:
+        s = "ISO_IR 101";
+        break;
+
+      case Encoding_Latin3:
+        s = "ISO_IR 109";
+        break;
+
+      case Encoding_Latin4:
+        s = "ISO_IR 110";
+        break;
+
+      case Encoding_Latin5:
+        s = "ISO_IR 148";
+        break;
+
+      case Encoding_Cyrillic:
+        s = "ISO_IR 144";
+        break;
+
+      case Encoding_Arabic:
+        s = "ISO_IR 127";
+        break;
+
+      case Encoding_Greek:
+        s = "ISO_IR 126";
+        break;
+
+      case Encoding_Hebrew:
+        s = "ISO_IR 138";
+        break;
+
+      case Encoding_Japanese:
+        s = "ISO_IR 13";
+        break;
+
+      case Encoding_Chinese:
+        s = "GB18030";
+        break;
+
+      case Encoding_Thai:
+        s = "ISO_IR 166";
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    Replace(DICOM_TAG_SPECIFIC_CHARACTER_SET, s, DicomReplaceMode_InsertIfAbsent);
+  }
+
+  void ParsedDicomFile::ToJson(Json::Value& target, bool simplify)
+  {
+    if (simplify)
+    {
+      Json::Value tmp;
+      FromDcmtkBridge::ToJson(tmp, *pimpl_->file_->getDataset());
+      SimplifyTags(target, tmp);
+    }
+    else
+    {
+      FromDcmtkBridge::ToJson(target, *pimpl_->file_->getDataset());
+    }
+  }
 }
--- a/OrthancServer/ParsedDicomFile.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ParsedDicomFile.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -51,6 +51,8 @@
     void Setup(const char* content,
                size_t size);
 
+    void RemovePrivateTagsInternal(const std::set<DicomTag>* toKeep);
+
   public:
     ParsedDicomFile();  // Create a minimal DICOM instance
 
@@ -79,7 +81,15 @@
                  const std::string& value,
                  DicomReplaceMode mode = DicomReplaceMode_InsertIfAbsent);
 
-    void RemovePrivateTags();
+    void RemovePrivateTags()
+    {
+      RemovePrivateTagsInternal(NULL);
+    }
+
+    void RemovePrivateTags(const std::set<DicomTag>& toKeep)
+    {
+      RemovePrivateTagsInternal(&toKeep);
+    }
 
     bool GetTagValue(std::string& value,
                      const DicomTag& tag);
@@ -104,6 +114,13 @@
     void ExtractPngImage(std::string& result,
                          unsigned int frame,
                          ImageExtractionMode mode);
+
+    Encoding GetEncoding() const;
+
+    void SetEncoding(Encoding encoding);
+
+    void ToJson(Json::Value& target, 
+                bool simplify);
   };
 
 }
--- a/OrthancServer/PrecompiledHeadersServer.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/PrecompiledHeadersServer.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/PrecompiledHeadersServer.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/PrecompiledHeadersServer.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/PrepareDatabase.sql	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/PrepareDatabase.sql	Thu May 21 16:58:30 2015 +0200
@@ -18,6 +18,15 @@
        PRIMARY KEY(id, tagGroup, tagElement)
        );
 
+-- The following table was added in Orthanc 0.8.5 (database v5)
+CREATE TABLE DicomIdentifiers(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       tagGroup INTEGER,
+       tagElement INTEGER,
+       value TEXT,
+       PRIMARY KEY(id, tagGroup, tagElement)
+       );
+
 CREATE TABLE Metadata(
        id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
        type INTEGER,
@@ -68,8 +77,14 @@
 CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
 
 CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id);
-CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement);
-CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY);
+-- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up
+-- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement);
+-- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY);
+
+-- The 3 following indexes were added in Orthanc 0.8.5 (database v5)
+CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
+CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
+CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY);
 
 CREATE INDEX ChangesIndex ON Changes(internalId);
 
@@ -85,6 +100,7 @@
 CREATE TRIGGER ResourceDeleted
 AFTER DELETE ON Resources
 BEGIN
+  SELECT SignalResourceDeleted(old.publicId, old.resourceType);  -- New in Orthanc 0.8.5 (db v5)
   SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
     FROM Resources AS parent WHERE internalId = old.parentId;
 END;
@@ -107,4 +123,4 @@
 
 -- Set the version of the database schema
 -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
-INSERT INTO GlobalProperties VALUES (1, "4");
+INSERT INTO GlobalProperties VALUES (1, "5");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ResourceFinder.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,383 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ResourceFinder.h"
+
+#include "FromDcmtkBridge.h"
+#include "ServerContext.h"
+
+#include <glog/logging.h>
+#include <boost/algorithm/string/predicate.hpp>
+
+namespace Orthanc
+{
+  class ResourceFinder::CandidateResources
+  {
+  private:
+    typedef std::map<DicomTag, std::string>  Query;
+
+    ResourceFinder&        finder_;
+    ServerIndex&           index_;
+    ResourceType           level_;
+    bool                   isFilterApplied_;
+    std::set<std::string>  filtered_;
+
+     
+    static void ListToSet(std::set<std::string>& target,
+                          const std::list<std::string>& source)
+    {
+      for (std::list<std::string>::const_iterator
+             it = source.begin(); it != source.end(); ++it)
+      {
+        target.insert(*it);
+      }
+    }
+
+
+  public:
+    CandidateResources(ResourceFinder& finder) : 
+      finder_(finder),
+      index_(finder.context_.GetIndex()),
+      level_(ResourceType_Patient), 
+      isFilterApplied_(false)
+    {
+    }
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void GoDown()
+    {
+      assert(level_ != ResourceType_Instance);
+
+      if (isFilterApplied_)
+      {
+        std::set<std::string> tmp = filtered_;
+
+        filtered_.clear();
+
+        for (std::set<std::string>::const_iterator 
+               it = tmp.begin(); it != tmp.end(); ++it)
+        {
+          std::list<std::string> children;
+          try
+          {
+            index_.GetChildren(children, *it);
+            ListToSet(filtered_, children);
+          }
+          catch (OrthancException&)
+          {
+            // The resource was removed in the meantime
+          }
+        }
+      }
+
+      switch (level_)
+      {
+        case ResourceType_Patient:
+          level_ = ResourceType_Study;
+          break;
+
+        case ResourceType_Study:
+          level_ = ResourceType_Series;
+          break;
+
+        case ResourceType_Series:
+          level_ = ResourceType_Instance;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+
+    void Flatten(std::list<std::string>& resources) const
+    {
+      resources.clear();
+
+      if (isFilterApplied_)
+      {
+        for (std::set<std::string>::const_iterator 
+               it = filtered_.begin(); it != filtered_.end(); ++it)
+        {
+          resources.push_back(*it);
+        }
+      }
+      else
+      {
+        index_.GetAllUuids(resources, level_);
+      }
+    }
+
+    
+    void RestrictIdentifier(const IQuery& query,
+                            const DicomTag& tag)
+    {
+      assert((level_ == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) ||
+             (level_ == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) ||
+             (level_ == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) ||
+             (level_ == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) ||
+             (level_ == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID));
+
+      std::string value;
+      if (!query.RestrictIdentifier(value, tag))
+      {
+        return;
+      }
+
+      LOG(INFO) << "Lookup for identifier tag "
+                << FromDcmtkBridge::GetName(tag) << " (value: " << value << ")";
+
+      std::list<std::string> resources;
+      index_.LookupIdentifier(resources, tag, value, level_);
+
+      if (isFilterApplied_)
+      {
+        std::set<std::string>  s;
+        ListToSet(s, resources);
+
+        std::set<std::string> tmp = filtered_;
+        filtered_.clear();
+
+        for (std::set<std::string>::const_iterator 
+               it = tmp.begin(); it != tmp.end(); ++it)
+        {
+          if (s.find(*it) != s.end())
+          {
+            filtered_.insert(*it);
+          }
+        }
+      }
+      else
+      {
+        assert(filtered_.empty());
+        isFilterApplied_ = true;
+        ListToSet(filtered_, resources);
+      }
+    }
+
+
+    void RestrictMainDicomTags(const IQuery& query)
+    {
+      if (!query.HasMainDicomTagsFilter(level_))
+      {
+        return;
+      }
+
+      std::list<std::string> resources;
+      Flatten(resources);
+
+      isFilterApplied_ = true;
+      filtered_.clear();
+
+      for (std::list<std::string>::const_iterator
+             it = resources.begin(); it != resources.end(); it++)
+      {
+        DicomMap mainTags;
+        if (index_.GetMainDicomTags(mainTags, *it, level_))
+        {
+          if (query.FilterMainDicomTags(*it, level_, mainTags))
+          {
+            filtered_.insert(*it);
+          }
+        }
+      }
+    }
+  };
+
+
+  ResourceFinder::ResourceFinder(ServerContext& context) : 
+    context_(context),
+    maxResults_(0)
+  {
+  }
+
+
+  void ResourceFinder::ApplyAtLevel(CandidateResources& candidates,
+                                    const IQuery& query,
+                                    ResourceType level)
+  {
+    if (level != ResourceType_Patient)
+    {
+      candidates.GoDown();
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+      {
+        candidates.RestrictIdentifier(query, DICOM_TAG_PATIENT_ID);
+        break;
+      }
+
+      case ResourceType_Study:
+      {
+        candidates.RestrictIdentifier(query, DICOM_TAG_STUDY_INSTANCE_UID);
+        candidates.RestrictIdentifier(query, DICOM_TAG_ACCESSION_NUMBER);
+        break;
+      }
+
+      case ResourceType_Series:
+      {
+        candidates.RestrictIdentifier(query, DICOM_TAG_SERIES_INSTANCE_UID);
+        break;
+      }
+
+      case ResourceType_Instance:
+      {
+        candidates.RestrictIdentifier(query, DICOM_TAG_SOP_INSTANCE_UID);
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    candidates.RestrictMainDicomTags(query);
+  }
+
+
+
+  static bool LookupOneInstance(std::string& result,
+                                ServerIndex& index,
+                                const std::string& id,
+                                ResourceType type)
+  {
+    if (type == ResourceType_Instance)
+    {
+      result = id;
+      return true;
+    }
+
+    std::string childId;
+    
+    {
+      std::list<std::string> children;
+      index.GetChildInstances(children, id);
+
+      if (children.empty())
+      {
+        return false;
+      }
+
+      childId = children.front();
+    }
+
+    return LookupOneInstance(result, index, childId, GetChildResourceType(type));
+  }
+
+
+  bool ResourceFinder::Apply(std::list<std::string>& result,
+                             const IQuery& query)
+  {
+    CandidateResources candidates(*this);
+
+    ApplyAtLevel(candidates, query, ResourceType_Patient);
+
+    const ResourceType level = query.GetLevel();
+
+    if (level == ResourceType_Study ||
+        level == ResourceType_Series ||
+        level == ResourceType_Instance)
+    {
+      ApplyAtLevel(candidates, query, ResourceType_Study);
+    }
+        
+    if (level == ResourceType_Series ||
+        level == ResourceType_Instance)
+    {
+      ApplyAtLevel(candidates, query, ResourceType_Series);
+    }
+        
+    if (level == ResourceType_Instance)
+    {
+      ApplyAtLevel(candidates, query, ResourceType_Instance);
+    }
+
+    if (!query.HasInstanceFilter())
+    {
+      candidates.Flatten(result);
+
+      if (maxResults_ != 0 &&
+          result.size() >= maxResults_)
+      {
+        result.resize(maxResults_);
+        return false;
+      }
+      else
+      {
+        return true;
+      }
+    }
+    else
+    {
+      std::list<std::string> tmp;
+      candidates.Flatten(tmp);
+      
+      result.clear();
+      for (std::list<std::string>::const_iterator 
+             resource = tmp.begin(); resource != tmp.end(); ++resource)
+      {
+        try
+        {
+          std::string instance;
+          if (LookupOneInstance(instance, context_.GetIndex(), *resource, level))
+          {
+            Json::Value content;
+            context_.ReadJson(content, instance);
+            if (query.FilterInstance(*resource, content))
+            {
+              result.push_back(*resource);
+
+              if (maxResults_ != 0 &&
+                  result.size() >= maxResults_)
+              {
+                // Too many results, stop before recording this new match
+                return false;
+              }
+            }
+          }
+        }
+        catch (OrthancException&)
+        {
+          // This resource has been deleted since the search was started
+        }
+      }      
+    }
+
+    return true;  // All the matching resources have been returned
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ResourceFinder.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,101 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ServerIndex.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ResourceFinder : public boost::noncopyable
+  {
+  public:
+    class IQuery : public boost::noncopyable
+    {
+    public:
+      virtual ~IQuery()
+      {
+      }
+
+      virtual ResourceType GetLevel() const = 0;
+
+      virtual bool RestrictIdentifier(std::string& value,
+                                      DicomTag identifier) const = 0;
+
+      virtual bool HasMainDicomTagsFilter(ResourceType level) const = 0;
+
+      virtual bool FilterMainDicomTags(const std::string& resourceId,
+                                       ResourceType level,
+                                       const DicomMap& mainTags) const = 0;
+
+      virtual bool HasInstanceFilter() const = 0;
+
+      virtual bool FilterInstance(const std::string& instanceId,
+                                  const Json::Value& content) const = 0;
+    };
+
+
+  private:
+    typedef std::map<DicomTag, std::string>  Identifiers;
+
+    class CandidateResources;
+
+    ServerContext&    context_;
+    size_t            maxResults_;
+
+    void ApplyAtLevel(CandidateResources& candidates,
+                      const IQuery& query,
+                      ResourceType level);
+
+  public:
+    ResourceFinder(ServerContext& context);
+
+    void SetMaxResults(size_t value)
+    {
+      maxResults_ = value;
+    }
+
+    size_t GetMaxResults() const
+    {
+      return maxResults_;
+    }
+
+    // Returns "true" iff. all the matching resources have been
+    // returned. Will be "false" if the results were truncated by
+    // "SetMaxResults()".
+    bool Apply(std::list<std::string>& result,
+               const IQuery& query);
+  };
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/CallSystemCommand.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,83 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "CallSystemCommand.h"
+
+#include <glog/logging.h>
+#include "../../Core/Toolbox.h"
+#include "../../Core/Uuid.h"
+
+namespace Orthanc
+{
+  CallSystemCommand::CallSystemCommand(ServerContext& context,
+                                       const std::string& command,
+                                       const std::vector<std::string>& arguments) : 
+    context_(context),
+    command_(command),
+    arguments_(arguments)
+  {
+  }
+
+  bool CallSystemCommand::Apply(ListOfStrings& outputs,
+                                const ListOfStrings& inputs)
+  {
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Calling system command " << command_ << " on instance " << *it;
+
+      try
+      {
+        std::string dicom;
+        context_.ReadFile(dicom, *it, FileContentType_Dicom);
+
+        Toolbox::TemporaryFile tmp;
+        tmp.Write(dicom);
+
+        std::vector<std::string> args = arguments_;
+        args.push_back(tmp.GetPath());
+
+        Toolbox::ExecuteSystemCommand(command_, args);
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(*it);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to call system command " << command_ 
+                   << " on instance " << *it << " in a Lua script: " << e.What();
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/CallSystemCommand.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,55 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "IServerCommand.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class CallSystemCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    std::string command_;
+    std::vector<std::string> arguments_;
+
+  public:
+    CallSystemCommand(ServerContext& context,
+                      const std::string& command,
+                      const std::vector<std::string>& arguments);
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/DeleteInstanceCommand.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,60 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "DeleteInstanceCommand.h"
+
+#include <glog/logging.h>
+
+namespace Orthanc
+{
+  bool DeleteInstanceCommand::Apply(ListOfStrings& outputs,
+                                    const ListOfStrings& inputs)
+  {
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Deleting instance " << *it;
+
+      try
+      {
+        Json::Value tmp;
+        context_.DeleteResource(tmp, *it, ResourceType_Instance);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to delete instance " << *it << " in a Lua script: " << e.What();
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/DeleteInstanceCommand.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,53 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "IServerCommand.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class DeleteInstanceCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+
+  public:
+    DeleteInstanceCommand(ServerContext& context) : context_(context)
+    {
+    }
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/IServerCommand.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,53 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 <list>
+#include <string>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IServerCommand : public boost::noncopyable
+  {
+  public:
+    typedef std::list<std::string>  ListOfStrings;
+
+    virtual ~IServerCommand()
+    {
+    }
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/ModifyInstanceCommand.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,103 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ModifyInstanceCommand.h"
+
+#include <glog/logging.h>
+
+namespace Orthanc
+{
+  ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context,
+                                               const DicomModification& modification) :
+    context_(context),
+    modification_(modification)
+  {
+    modification_.SetAllowManualIdentifiers(true);
+
+    if (modification_.IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      modification_.SetLevel(ResourceType_Patient);
+    }
+    else if (modification_.IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      modification_.SetLevel(ResourceType_Study);
+    }
+    else if (modification_.IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      modification_.SetLevel(ResourceType_Series);
+    }
+    else
+    {
+      modification_.SetLevel(ResourceType_Instance);
+    }
+  }
+
+
+  bool ModifyInstanceCommand::Apply(ListOfStrings& outputs,
+                                    const ListOfStrings& inputs)
+  {
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Modifying resource " << *it;
+
+      try
+      {
+        std::auto_ptr<ParsedDicomFile> modified;
+
+        {
+          ServerContext::DicomCacheLocker lock(context_, *it);
+          modified.reset(lock.GetDicom().Clone());
+        }
+
+        modification_.Apply(*modified);
+
+        DicomInstanceToStore toStore;
+        toStore.SetParsedDicomFile(*modified);
+        // TODO other metadata
+        toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it);
+
+        std::string modifiedId;
+        context_.Store(modifiedId, toStore);
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(modifiedId);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to modify instance " << *it << " in a Lua script: " << e.What();
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/ModifyInstanceCommand.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,59 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "IServerCommand.h"
+#include "../ServerContext.h"
+#include "../DicomModification.h"
+
+namespace Orthanc
+{
+  class ModifyInstanceCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    DicomModification modification_;
+
+  public:
+    ModifyInstanceCommand(ServerContext& context,
+                          const DicomModification& modification);
+
+    const DicomModification& GetModification() const
+    {
+      return modification_;
+    }
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/ServerCommandInstance.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,97 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ServerCommandInstance.h"
+
+#include "../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  bool ServerCommandInstance::Execute(IListener& listener)
+  {
+    ListOfStrings outputs;
+
+    bool success = false;
+
+    try
+    {
+      if (command_->Apply(outputs, inputs_))
+      {
+        success = true;
+      }
+    }
+    catch (OrthancException&)
+    {
+    }
+
+    if (!success)
+    {
+      listener.SignalFailure(jobId_);
+      return true;
+    }
+
+    for (std::list<ServerCommandInstance*>::iterator
+           it = next_.begin(); it != next_.end(); ++it)
+    {
+      for (ListOfStrings::const_iterator
+             output = outputs.begin(); output != outputs.end(); ++output)
+      {
+        (*it)->AddInput(*output);
+      }
+    }
+
+    listener.SignalSuccess(jobId_);
+    return true;
+  }
+
+
+  ServerCommandInstance::ServerCommandInstance(IServerCommand *command,
+                                               const std::string& jobId) : 
+    command_(command), 
+    jobId_(jobId),
+    connectedToSink_(false)
+  {
+    if (command_ == NULL)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  ServerCommandInstance::~ServerCommandInstance()
+  {
+    if (command_ != NULL)
+    {
+      delete command_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/ServerCommandInstance.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,104 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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/IDynamicObject.h"
+#include "IServerCommand.h"
+
+namespace Orthanc
+{
+  class ServerCommandInstance : public IDynamicObject
+  {
+    friend class ServerScheduler;
+
+  public:
+    class IListener
+    {
+    public:
+      virtual ~IListener()
+      {
+      }
+
+      virtual void SignalSuccess(const std::string& jobId) = 0;
+
+      virtual void SignalFailure(const std::string& jobId) = 0;
+    };
+
+  private:
+    typedef IServerCommand::ListOfStrings  ListOfStrings;
+
+    IServerCommand *command_;
+    std::string jobId_;
+    ListOfStrings inputs_;
+    std::list<ServerCommandInstance*> next_;
+    bool connectedToSink_;
+
+    bool Execute(IListener& listener);
+
+  public:
+    ServerCommandInstance(IServerCommand *command,
+                          const std::string& jobId);
+
+    virtual ~ServerCommandInstance();
+
+    const std::string& GetJobId() const
+    {
+      return jobId_;
+    }
+
+    void AddInput(const std::string& input)
+    {
+      inputs_.push_back(input);
+    }
+
+    void ConnectOutput(ServerCommandInstance& next)
+    {
+      next_.push_back(&next);
+    }
+
+    void SetConnectedToSink(bool connected = true)
+    {
+      connectedToSink_ = connected;
+    }
+
+    bool IsConnectedToSink() const
+    {
+      return connectedToSink_;
+    }
+
+    const std::list<ServerCommandInstance*>& GetNextCommands() const
+    {
+      return next_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/ServerJob.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,146 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ServerJob.h"
+
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
+#include "../../Core/Uuid.h"
+
+namespace Orthanc
+{
+  void ServerJob::CheckOrdering()
+  {
+    std::map<ServerCommandInstance*, unsigned int> index;
+
+    unsigned int count = 0;
+    for (std::list<ServerCommandInstance*>::const_iterator
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      index[*it] = count++;
+    }
+
+    for (std::list<ServerCommandInstance*>::const_iterator
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      const std::list<ServerCommandInstance*>& nextCommands = (*it)->GetNextCommands();
+
+      for (std::list<ServerCommandInstance*>::const_iterator
+             next = nextCommands.begin(); next != nextCommands.end(); ++next)
+      {
+        if (index.find(*next) == index.end() ||
+            index[*next] <= index[*it])
+        {
+          // You must reorder your calls to "ServerJob::AddCommand"
+          throw OrthancException("Bad ordering of filters in a job");
+        }
+      }
+    }
+  }
+
+
+  size_t ServerJob::Submit(SharedMessageQueue& target,
+                           ServerCommandInstance::IListener& listener)
+  {
+    if (submitted_)
+    {
+      // This job has already been submitted
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    CheckOrdering();
+
+    size_t size = filters_.size();
+
+    for (std::list<ServerCommandInstance*>::iterator 
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      target.Enqueue(*it);
+    }
+
+    filters_.clear();
+    submitted_ = true;
+
+    return size;
+  }
+
+
+  ServerJob::ServerJob() :
+    jobId_(Toolbox::GenerateUuid()),
+    submitted_(false),
+    description_("no description")
+  {
+  }
+
+
+  ServerJob::~ServerJob()
+  {
+    for (std::list<ServerCommandInstance*>::iterator
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      delete *it;
+    }
+
+    for (std::list<IDynamicObject*>::iterator
+           it = payloads_.begin(); it != payloads_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+
+  ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter)
+  {
+    if (submitted_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    filters_.push_back(new ServerCommandInstance(filter, jobId_));
+      
+    return *filters_.back();
+  }
+
+
+  IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload)
+  {
+    if (submitted_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    payloads_.push_back(payload);
+      
+    return *filters_.back();
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/ServerJob.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,82 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ServerCommandInstance.h"
+#include "../../Core/MultiThreading/SharedMessageQueue.h"
+
+namespace Orthanc
+{
+  class ServerJob
+  {
+    friend class ServerScheduler;
+
+  private:
+    std::list<ServerCommandInstance*> filters_;
+    std::list<IDynamicObject*> payloads_;
+    std::string jobId_;
+    bool submitted_;
+    std::string description_;
+
+    void CheckOrdering();
+
+    size_t Submit(SharedMessageQueue& target,
+                  ServerCommandInstance::IListener& listener);
+
+  public:
+    ServerJob();
+
+    ~ServerJob();
+
+    const std::string& GetId() const
+    {
+      return jobId_;
+    }
+
+    void SetDescription(const std::string& description)
+    {
+      description_ = description;
+    }
+
+    const std::string& GetDescription() const
+    {
+      return description_;
+    }
+
+    ServerCommandInstance& AddCommand(IServerCommand* filter);
+
+    // Take the ownership of a payload to a job. This payload will be
+    // automatically freed when the job succeeds or fails.
+    IDynamicObject& AddPayload(IDynamicObject* payload);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/ServerScheduler.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,336 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ServerScheduler.h"
+
+#include "../../Core/OrthancException.h"
+
+#include <glog/logging.h>
+
+namespace Orthanc
+{
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+    class Sink : public IServerCommand
+    {
+    private:
+      ListOfStrings& target_;
+
+    public:
+      Sink(ListOfStrings& target) : target_(target)
+      {
+      }
+
+      virtual bool Apply(ListOfStrings& outputs,
+                         const ListOfStrings& inputs)
+      {
+        for (ListOfStrings::const_iterator 
+               it = inputs.begin(); it != inputs.end(); ++it)
+        {
+          target_.push_back(*it);
+        }
+
+        return true;
+      }    
+    };
+  }
+
+
+  ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId)
+  {
+    Jobs::iterator info = jobs_.find(jobId);
+
+    if (info == jobs_.end())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return info->second;
+  }
+
+
+  void ServerScheduler::SignalSuccess(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    JobInfo& info = GetJobInfo(jobId);
+    info.success_++;
+
+    assert(info.failures_ == 0);
+
+    if (info.success_ >= info.size_)
+    {
+      if (info.watched_)
+      {
+        watchedJobStatus_[jobId] = JobStatus_Success;
+        watchedJobFinished_.notify_all();
+      }
+
+      LOG(INFO) << "Job successfully finished (" << info.description_ << ")";
+      jobs_.erase(jobId);
+
+      availableJob_.Release();
+    }
+  }
+
+
+  void ServerScheduler::SignalFailure(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    JobInfo& info = GetJobInfo(jobId);
+    info.failures_++;
+
+    if (info.success_ + info.failures_ >= info.size_)
+    {
+      if (info.watched_)
+      {
+        watchedJobStatus_[jobId] = JobStatus_Failure;
+        watchedJobFinished_.notify_all();
+      }
+
+      LOG(ERROR) << "Job has failed (" << info.description_ << ")";
+      jobs_.erase(jobId);
+
+      availableJob_.Release();
+    }
+  }
+
+
+  void ServerScheduler::Worker(ServerScheduler* that)
+  {
+    static const int32_t TIMEOUT = 100;
+
+    LOG(WARNING) << "The server scheduler has started";
+
+    while (!that->finish_)
+    {
+      std::auto_ptr<IDynamicObject> object(that->queue_.Dequeue(TIMEOUT));
+      if (object.get() != NULL)
+      {
+        ServerCommandInstance& filter = dynamic_cast<ServerCommandInstance&>(*object);
+
+        // Skip the execution of this filter if its parent job has
+        // previously failed.
+        bool jobHasFailed;
+        {
+          boost::mutex::scoped_lock lock(that->mutex_);
+          JobInfo& info = that->GetJobInfo(filter.GetJobId());
+          jobHasFailed = (info.failures_ > 0 || info.cancel_); 
+        }
+
+        if (jobHasFailed)
+        {
+          that->SignalFailure(filter.GetJobId());
+        }
+        else
+        {
+          filter.Execute(*that);
+        }
+      }
+    }
+  }
+
+
+  void ServerScheduler::SubmitInternal(ServerJob& job,
+                                       bool watched)
+  {
+    availableJob_.Acquire();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    JobInfo info;
+    info.size_ = job.Submit(queue_, *this);
+    info.cancel_ = false;
+    info.success_ = 0;
+    info.failures_ = 0;
+    info.description_ = job.GetDescription();
+    info.watched_ = watched;
+
+    assert(info.size_ > 0);
+
+    if (watched)
+    {
+      watchedJobStatus_[job.GetId()] = JobStatus_Running;
+    }
+
+    jobs_[job.GetId()] = info;
+
+    LOG(INFO) << "New job submitted (" << job.description_ << ")";
+  }
+
+
+  ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs)
+  {
+    finish_ = false;
+    worker_ = boost::thread(Worker, this);
+  }
+
+
+  ServerScheduler::~ServerScheduler()
+  {
+    finish_ = true;
+    worker_.join();
+  }
+
+
+  void ServerScheduler::Submit(ServerJob& job)
+  {
+    if (job.filters_.empty())
+    {
+      return;
+    }
+
+    SubmitInternal(job, false);
+  }
+
+
+  bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs,
+                                      ServerJob& job)
+  {
+    std::string jobId = job.GetId();
+
+    outputs.clear();
+
+    if (job.filters_.empty())
+    {
+      return true;
+    }
+
+    // Add a sink filter to collect all the results of the filters
+    // that have no next filter.
+    ServerCommandInstance& sink = job.AddCommand(new Sink(outputs));
+
+    for (std::list<ServerCommandInstance*>::iterator
+           it = job.filters_.begin(); it != job.filters_.end(); ++it)
+    {
+      if ((*it) != &sink &&
+          (*it)->IsConnectedToSink())
+      {
+        (*it)->ConnectOutput(sink);
+      }
+    }
+
+    // Submit the job
+    SubmitInternal(job, true);
+
+    // Wait for the job to complete (either success or failure)
+    JobStatus status;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end());
+        
+      while (watchedJobStatus_[jobId] == JobStatus_Running)
+      {
+        watchedJobFinished_.wait(lock);
+      }
+
+      status = watchedJobStatus_[jobId];
+      watchedJobStatus_.erase(jobId);
+    }
+
+    return (status == JobStatus_Success);
+  }
+
+
+  bool ServerScheduler::SubmitAndWait(ServerJob& job)
+  {
+    ListOfStrings ignoredSink;
+    return SubmitAndWait(ignoredSink, job);
+  }
+
+
+  bool ServerScheduler::IsRunning(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return jobs_.find(jobId) != jobs_.end();
+  }
+
+
+  void ServerScheduler::Cancel(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Jobs::iterator job = jobs_.find(jobId);
+
+    if (job != jobs_.end())
+    {
+      job->second.cancel_ = true;
+      LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")";
+    }
+  }
+
+
+  float ServerScheduler::GetProgress(const std::string& jobId) 
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Jobs::iterator job = jobs_.find(jobId);
+
+    if (job == jobs_.end() || 
+        job->second.size_ == 0  /* should never happen */)
+    {
+      // This job is not running
+      return 1;
+    }
+
+    if (job->second.failures_ != 0)
+    {
+      return 1;
+    }
+
+    if (job->second.size_ == 1)
+    {
+      return job->second.success_;
+    }
+
+    return (static_cast<float>(job->second.success_) / 
+            static_cast<float>(job->second.size_ - 1));
+  }
+
+
+  void ServerScheduler::GetListOfJobs(ListOfStrings& jobs)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    jobs.clear();
+
+    for (Jobs::const_iterator 
+           it = jobs_.begin(); it != jobs_.end(); ++it)
+    {
+      jobs.push_back(it->first);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/ServerScheduler.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,120 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "ServerJob.h"
+
+#include "../../Core/MultiThreading/Semaphore.h"
+
+namespace Orthanc
+{
+  class ServerScheduler : public ServerCommandInstance::IListener
+  {
+  private:
+    struct JobInfo
+    {
+      bool watched_;
+      bool cancel_;
+      size_t size_;
+      size_t success_;
+      size_t failures_;
+      std::string description_;
+    };
+
+    enum JobStatus
+    {
+      JobStatus_Running = 1,
+      JobStatus_Success = 2,
+      JobStatus_Failure = 3
+    };
+
+    typedef IServerCommand::ListOfStrings  ListOfStrings;
+    typedef std::map<std::string, JobInfo> Jobs;
+
+    boost::mutex mutex_;
+    boost::condition_variable watchedJobFinished_;
+    Jobs jobs_;
+    SharedMessageQueue queue_;
+    bool finish_;
+    boost::thread worker_;
+    std::map<std::string, JobStatus> watchedJobStatus_;
+    Semaphore availableJob_;
+
+    JobInfo& GetJobInfo(const std::string& jobId);
+
+    virtual void SignalSuccess(const std::string& jobId);
+
+    virtual void SignalFailure(const std::string& jobId);
+
+    static void Worker(ServerScheduler* that);
+
+    void SubmitInternal(ServerJob& job,
+                        bool watched);
+
+  public:
+    ServerScheduler(unsigned int maxjobs);
+
+    ~ServerScheduler();
+
+    void Submit(ServerJob& job);
+
+    bool SubmitAndWait(ListOfStrings& outputs,
+                       ServerJob& job);
+
+    bool SubmitAndWait(ServerJob& job);
+
+    bool IsRunning(const std::string& jobId);
+
+    void Cancel(const std::string& jobId);
+
+    // Returns a number between 0 and 1
+    float GetProgress(const std::string& jobId);
+
+    bool IsRunning(const ServerJob& job)
+    {
+      return IsRunning(job.GetId());
+    }
+
+    void Cancel(const ServerJob& job) 
+    {
+      Cancel(job.GetId());
+    }
+
+    float GetProgress(const ServerJob& job) 
+    {
+      return GetProgress(job.GetId());
+    }
+
+    void GetListOfJobs(ListOfStrings& jobs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/StorePeerCommand.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,100 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "StorePeerCommand.h"
+
+#include "../../Core/HttpClient.h"
+
+#include <glog/logging.h>
+
+namespace Orthanc
+{
+  StorePeerCommand::StorePeerCommand(ServerContext& context,
+                                     const OrthancPeerParameters& peer,
+                                     bool ignoreExceptions) : 
+    context_(context),
+    peer_(peer),
+    ignoreExceptions_(ignoreExceptions)
+  {
+  }
+
+  bool StorePeerCommand::Apply(ListOfStrings& outputs,
+                               const ListOfStrings& inputs)
+  {
+    // Configure the HTTP client
+    HttpClient client;
+    client.SetProxy(Configuration::GetGlobalStringParameter("HttpProxy", ""));
+    if (peer_.GetUsername().size() != 0 && 
+        peer_.GetPassword().size() != 0)
+    {
+      client.SetCredentials(peer_.GetUsername().c_str(), 
+                            peer_.GetPassword().c_str());
+    }
+
+    client.SetUrl(peer_.GetUrl() + "instances");
+    client.SetMethod(HttpMethod_Post);
+
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Sending resource " << *it << " to peer \"" 
+                << peer_.GetUrl() << "\"";
+
+      try
+      {
+        context_.ReadFile(client.AccessPostData(), *it, FileContentType_Dicom);
+
+        std::string answer;
+        if (!client.Apply(answer))
+        {
+          LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\"";
+          throw OrthancException(ErrorCode_NetworkProtocol);
+        }
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(*it);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to forward to an Orthanc peer in a Lua script (instance " 
+                   << *it << ", peer " << peer_.GetUrl() << "): " << e.What();
+
+        if (!ignoreExceptions_)
+        {
+          throw;
+        }
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/StorePeerCommand.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,56 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "IServerCommand.h"
+#include "../ServerContext.h"
+#include "../OrthancInitialization.h"
+
+namespace Orthanc
+{
+  class StorePeerCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    OrthancPeerParameters peer_;
+    bool ignoreExceptions_;
+
+  public:
+    StorePeerCommand(ServerContext& context,
+                     const OrthancPeerParameters& peer,
+                     bool ignoreExceptions);
+    
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/StoreScuCommand.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,84 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "StoreScuCommand.h"
+
+#include <glog/logging.h>
+
+namespace Orthanc
+{
+  StoreScuCommand::StoreScuCommand(ServerContext& context,
+                                   const RemoteModalityParameters& modality,
+                                   bool ignoreExceptions) : 
+    context_(context),
+    modality_(modality),
+    ignoreExceptions_(ignoreExceptions)
+  {
+  }
+
+  bool StoreScuCommand::Apply(ListOfStrings& outputs,
+                             const ListOfStrings& inputs)
+  {
+    ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), modality_);
+
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Sending resource " << *it << " to modality \"" 
+                << modality_.GetApplicationEntityTitle() << "\"";
+
+      try
+      {
+        std::string dicom;
+        context_.ReadFile(dicom, *it, FileContentType_Dicom);
+        locker.GetConnection().Store(dicom);
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(*it);
+      }
+      catch (OrthancException& e)
+      {
+        // Ignore transmission errors (e.g. if the remote modality is
+        // powered off)
+        LOG(ERROR) << "Unable to forward to a modality in a Lua script (instance " 
+                   << *it << "): " << e.What();
+
+        if (!ignoreExceptions_)
+        {
+          throw;
+        }
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Scheduler/StoreScuCommand.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,55 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "IServerCommand.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class StoreScuCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    RemoteModalityParameters modality_;
+    bool ignoreExceptions_;
+
+  public:
+    StoreScuCommand(ServerContext& context,
+                    const RemoteModalityParameters& modality,
+                    bool ignoreExceptions);
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- a/OrthancServer/ServerContext.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ServerContext.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -43,9 +43,20 @@
 #include <EmbeddedResources.h>
 #include <dcmtk/dcmdata/dcfilefo.h>
 
+
+#include "Scheduler/CallSystemCommand.h"
+#include "Scheduler/DeleteInstanceCommand.h"
+#include "Scheduler/ModifyInstanceCommand.h"
+#include "Scheduler/StoreScuCommand.h"
+#include "Scheduler/StorePeerCommand.h"
+#include "OrthancRestApi/OrthancRestApi.h"
+#include "../Plugins/Engine/OrthancPlugins.h"
+
+
 #define ENABLE_DICOM_CACHE  1
 
 static const char* RECEIVED_INSTANCE_FILTER = "ReceivedInstanceFilter";
+static const char* ON_STORED_INSTANCE = "OnStoredInstance";
 
 static const size_t DICOM_CACHE_SIZE = 2;
 
@@ -60,19 +71,22 @@
 
 namespace Orthanc
 {
-  ServerContext::ServerContext(const boost::filesystem::path& storagePath,
-                               const boost::filesystem::path& indexPath) :
-    storage_(storagePath.string()),
-    index_(*this, indexPath.string()),
-    accessor_(storage_),
+  ServerContext::ServerContext(IDatabaseWrapper& database) :
+    index_(*this, database),
     compressionEnabled_(false),
     provider_(*this),
-    dicomCache_(provider_, DICOM_CACHE_SIZE)
+    dicomCache_(provider_, DICOM_CACHE_SIZE),
+    scheduler_(Configuration::GetGlobalIntegerParameter("LimitJobs", 10)),
+    plugins_(NULL),
+    pluginsManager_(NULL)
   {
     scu_.SetLocalApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"));
-    //scu_.SetMillisecondsBeforeClose(1);  // The connection is always released
+
+    uint64_t s = Configuration::GetGlobalIntegerParameter("DicomAssociationCloseDelay", 5);  // In seconds
+    scu_.SetMillisecondsBeforeClose(s * 1000);  // Milliseconds are expected here
 
     lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
+    lua_.SetHttpProxy(Configuration::GetGlobalStringParameter("HttpProxy", ""));
   }
 
   void ServerContext::SetCompressionEnabled(bool enabled)
@@ -85,84 +99,312 @@
     compressionEnabled_ = enabled;
   }
 
-  void ServerContext::RemoveFile(const std::string& fileUuid)
+  void ServerContext::RemoveFile(const std::string& fileUuid,
+                                 FileContentType type)
   {
-    storage_.Remove(fileUuid);
+    accessor_.Remove(fileUuid, type);
   }
 
-  StoreStatus ServerContext::Store(const char* dicomInstance,
-                                   size_t dicomSize,
-                                   const DicomMap& dicomSummary,
-                                   const Json::Value& dicomJson,
-                                   const std::string& remoteAet)
+
+  bool ServerContext::ApplyReceivedInstanceFilter(const Json::Value& simplified,
+                                                  const std::string& remoteAet)
   {
-    // Test if the instance must be filtered out
-    if (lua_.IsExistingFunction(RECEIVED_INSTANCE_FILTER))
+    LuaContextLocker locker(*this);
+
+    if (locker.GetLua().IsExistingFunction(RECEIVED_INSTANCE_FILTER))
     {
-      Json::Value simplified;
-      SimplifyTags(simplified, dicomJson);
-
-      LuaFunctionCall call(lua_, RECEIVED_INSTANCE_FILTER);
-      call.PushJSON(simplified);
+      LuaFunctionCall call(locker.GetLua(), RECEIVED_INSTANCE_FILTER);
+      call.PushJson(simplified);
       call.PushString(remoteAet);
 
       if (!call.ExecutePredicate())
       {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  static IServerCommand* ParseOperation(ServerContext& context,
+                                        const std::string& operation,
+                                        const Json::Value& parameters)
+  {
+    if (operation == "delete")
+    {
+      LOG(INFO) << "Lua script to delete instance " << parameters["Instance"].asString();
+      return new DeleteInstanceCommand(context);
+    }
+
+    if (operation == "store-scu")
+    {
+      std::string modality = parameters["Modality"].asString();
+      LOG(INFO) << "Lua script to send instance " << parameters["Instance"].asString()
+                << " to modality " << modality << " using Store-SCU";
+      return new StoreScuCommand(context, Configuration::GetModalityUsingSymbolicName(modality), true);
+    }
+
+    if (operation == "store-peer")
+    {
+      std::string peer = parameters["Peer"].asString();
+      LOG(INFO) << "Lua script to send instance " << parameters["Instance"].asString()
+                << " to peer " << peer << " using HTTP";
+
+      OrthancPeerParameters parameters;
+      Configuration::GetOrthancPeer(parameters, peer);
+      return new StorePeerCommand(context, parameters, true);
+    }
+
+    if (operation == "modify")
+    {
+      LOG(INFO) << "Lua script to modify instance " << parameters["Instance"].asString();
+      DicomModification modification;
+      OrthancRestApi::ParseModifyRequest(modification, parameters);
+
+      std::auto_ptr<ModifyInstanceCommand> command(new ModifyInstanceCommand(context, modification));
+      return command.release();
+    }
+
+    if (operation == "call-system")
+    {
+      LOG(INFO) << "Lua script to call system command on " << parameters["Instance"].asString();
+
+      const Json::Value& argsIn = parameters["Arguments"];
+      if (argsIn.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadParameterType);
+      }
+
+      std::vector<std::string> args;
+      args.reserve(argsIn.size());
+      for (Json::Value::ArrayIndex i = 0; i < argsIn.size(); ++i)
+      {
+        // http://jsoncpp.sourceforge.net/namespace_json.html#7d654b75c16a57007925868e38212b4e
+        switch (argsIn[i].type())
+        {
+          case Json::stringValue:
+            args.push_back(argsIn[i].asString());
+            break;
+
+          case Json::intValue:
+            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asInt()));
+            break;
+
+          case Json::uintValue:
+            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asUInt()));
+            break;
+
+          case Json::realValue:
+            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asFloat()));
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_BadParameterType);
+        }
+      }
+
+      return new CallSystemCommand(context, parameters["Command"].asString(), args);
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+
+
+  void ServerContext::ApplyLuaOnStoredInstance(const std::string& instanceId,
+                                               const Json::Value& simplifiedDicom,
+                                               const Json::Value& metadata,
+                                               const std::string& remoteAet,
+                                               const std::string& calledAet)
+  {
+    LuaContextLocker locker(*this);
+
+    if (locker.GetLua().IsExistingFunction(ON_STORED_INSTANCE))
+    {
+      locker.GetLua().Execute("_InitializeJob()");
+
+      LuaFunctionCall call(locker.GetLua(), ON_STORED_INSTANCE);
+      call.PushString(instanceId);
+      call.PushJson(simplifiedDicom);
+      call.PushJson(metadata);
+      call.PushJson(remoteAet);
+      call.PushJson(calledAet);
+      call.Execute();
+
+      Json::Value operations;
+      LuaFunctionCall call2(locker.GetLua(), "_AccessJob");
+      call2.ExecuteToJson(operations);
+     
+      if (operations.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      ServerJob job;
+      ServerCommandInstance* previousCommand = NULL;
+
+      for (Json::Value::ArrayIndex i = 0; i < operations.size(); ++i)
+      {
+        if (operations[i].type() != Json::objectValue ||
+            !operations[i].isMember("Operation"))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        const Json::Value& parameters = operations[i];
+        std::string operation = parameters["Operation"].asString();
+
+        ServerCommandInstance& command = job.AddCommand(ParseOperation(*this, operation, operations[i]));
+        
+        if (!parameters.isMember("Instance"))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        std::string instance = parameters["Instance"].asString();
+        if (instance.empty())
+        {
+          previousCommand->ConnectOutput(command);
+        }
+        else 
+        {
+          command.AddInput(instance);
+        }
+
+        previousCommand = &command;
+      }
+
+      job.SetDescription(std::string("Lua script: ") + ON_STORED_INSTANCE);
+      scheduler_.Submit(job);
+    }
+  }
+
+
+  StoreStatus ServerContext::Store(std::string& resultPublicId,
+                                   DicomInstanceToStore& dicom)
+  {
+    try
+    {
+      DicomInstanceHasher hasher(dicom.GetSummary());
+      resultPublicId = hasher.HashInstance();
+
+      Json::Value simplified;
+      SimplifyTags(simplified, dicom.GetJson());
+
+      // Test if the instance must be filtered out
+      if (!ApplyReceivedInstanceFilter(simplified, dicom.GetRemoteAet()))
+      {
         LOG(INFO) << "An incoming instance has been discarded by the filter";
         return StoreStatus_FilteredOut;
       }
-    }
+
+      if (compressionEnabled_)
+      {
+        accessor_.SetCompressionForNextOperations(CompressionType_Zlib);
+      }
+      else
+      {
+        accessor_.SetCompressionForNextOperations(CompressionType_None);
+      }      
+
+      FileInfo dicomInfo = accessor_.Write(dicom.GetBufferData(), dicom.GetBufferSize(), FileContentType_Dicom);
+      FileInfo jsonInfo = accessor_.Write(dicom.GetJson().toStyledString(), FileContentType_DicomAsJson);
+
+      ServerIndex::Attachments attachments;
+      attachments.push_back(dicomInfo);
+      attachments.push_back(jsonInfo);
+
+      typedef std::map<MetadataType, std::string>  InstanceMetadata;
+      InstanceMetadata  instanceMetadata;
+      StoreStatus status = index_.Store(instanceMetadata, dicom.GetSummary(), attachments, 
+                                        dicom.GetRemoteAet(), dicom.GetMetadata());
 
-    if (compressionEnabled_)
-    {
-      accessor_.SetCompressionForNextOperations(CompressionType_Zlib);
-    }
-    else
-    {
-      accessor_.SetCompressionForNextOperations(CompressionType_None);
-    }      
+      dicom.GetMetadata().clear();
+
+      for (InstanceMetadata::const_iterator it = instanceMetadata.begin();
+           it != instanceMetadata.end(); ++it)
+      {
+        dicom.GetMetadata().insert(std::make_pair(std::make_pair(ResourceType_Instance, it->first),
+                                                  it->second));
+      }
+            
+      if (status != StoreStatus_Success)
+      {
+        accessor_.Remove(dicomInfo.GetUuid(), FileContentType_Dicom);
+        accessor_.Remove(jsonInfo.GetUuid(), FileContentType_DicomAsJson);
+      }
+
+      switch (status)
+      {
+        case StoreStatus_Success:
+          LOG(INFO) << "New instance stored";
+          break;
+
+        case StoreStatus_AlreadyStored:
+          LOG(INFO) << "Already stored";
+          break;
 
-    FileInfo dicomInfo = accessor_.Write(dicomInstance, dicomSize, FileContentType_Dicom);
-    FileInfo jsonInfo = accessor_.Write(dicomJson.toStyledString(), FileContentType_DicomAsJson);
+        case StoreStatus_Failure:
+          LOG(ERROR) << "Store failure";
+          break;
+
+        default:
+          // This should never happen
+          break;
+      }
+
+      if (status == StoreStatus_Success ||
+          status == StoreStatus_AlreadyStored)
+      {
+        Json::Value metadata = Json::objectValue;
+        for (std::map<MetadataType, std::string>::const_iterator 
+               it = instanceMetadata.begin(); 
+             it != instanceMetadata.end(); ++it)
+        {
+          metadata[EnumerationToString(it->first)] = it->second;
+        }
 
-    ServerIndex::Attachments attachments;
-    attachments.push_back(dicomInfo);
-    attachments.push_back(jsonInfo);
+        try
+        {
+          ApplyLuaOnStoredInstance(resultPublicId, simplified, metadata, 
+                                   dicom.GetRemoteAet(), dicom.GetCalledAet());
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Error in " << ON_STORED_INSTANCE << " callback (Lua): " << e.What();
+        }
 
-    StoreStatus status = index_.Store(dicomSummary, attachments, remoteAet);
+        if (plugins_ != NULL)
+        {
+          try
+          {
+            plugins_->SignalStoredInstance(dicom, resultPublicId);
+          }
+          catch (OrthancException& e)
+          {
+            LOG(ERROR) << "Error in " << ON_STORED_INSTANCE << " callback (plugins): " << e.What();
+          }
+        }
+      }
 
-    if (status != StoreStatus_Success)
+      return status;
+    }
+    catch (OrthancException& e)
     {
-      storage_.Remove(dicomInfo.GetUuid());
-      storage_.Remove(jsonInfo.GetUuid());
-    }
-
-    switch (status)
-    {
-      case StoreStatus_Success:
-        LOG(INFO) << "New instance stored";
-        break;
+      if (e.GetErrorCode() == ErrorCode_InexistentTag)
+      {
+        LogMissingRequiredTag(dicom.GetSummary());
+      }
 
-      case StoreStatus_AlreadyStored:
-        LOG(INFO) << "Already stored";
-        break;
-
-      case StoreStatus_Failure:
-        LOG(ERROR) << "Store failure";
-        break;
-
-      default:
-        // This should never happen
-        break;
+      throw;
     }
-
-    return status;
   }
 
-  
-  void ServerContext::AnswerDicomFile(RestApiOutput& output,
-                                      const std::string& instancePublicId,
-                                      FileContentType content)
+
+
+  void ServerContext::AnswerAttachment(RestApiOutput& output,
+                                       const std::string& instancePublicId,
+                                       FileContentType content)
   {
     FileInfo attachment;
     if (!index_.LookupAttachment(attachment, instancePublicId, content))
@@ -172,8 +414,8 @@
 
     accessor_.SetCompressionForNextOperations(attachment.GetCompressionType());
 
-    std::auto_ptr<HttpFileSender> sender(accessor_.ConstructHttpFileSender(attachment.GetUuid()));
-    sender->SetContentType("application/dicom");
+    std::auto_ptr<HttpFileSender> sender(accessor_.ConstructHttpFileSender(attachment.GetUuid(), attachment.GetContentType()));
+    sender->SetContentType(GetMimeType(content));
     sender->SetDownloadFilename(instancePublicId + ".dcm");
     output.AnswerFile(*sender);
   }
@@ -213,7 +455,7 @@
       accessor_.SetCompressionForNextOperations(CompressionType_None);
     }
 
-    accessor_.Read(result, attachment.GetUuid());
+    accessor_.Read(result, attachment.GetUuid(), attachment.GetContentType());
   }
 
 
@@ -227,14 +469,14 @@
 
   ServerContext::DicomCacheLocker::DicomCacheLocker(ServerContext& that,
                                                     const std::string& instancePublicId) : 
-    that_(that)
+    that_(that),
+    lock_(that_.dicomCacheMutex_)
   {
 #if ENABLE_DICOM_CACHE == 0
     static std::auto_ptr<IDynamicObject> p;
     p.reset(provider_.Provide(instancePublicId));
     dicom_ = dynamic_cast<ParsedDicomFile*>(p.get());
 #else
-    that_.dicomCacheMutex_.lock();
     dicom_ = &dynamic_cast<ParsedDicomFile&>(that_.dicomCache_.Access(instancePublicId));
 #endif
   }
@@ -242,91 +484,6 @@
 
   ServerContext::DicomCacheLocker::~DicomCacheLocker()
   {
-#if ENABLE_DICOM_CACHE == 0
-#else
-    that_.dicomCacheMutex_.unlock();
-#endif
-  }
-
-
-  static DcmFileFormat& GetDicom(ParsedDicomFile& file)
-  {
-    return *reinterpret_cast<DcmFileFormat*>(file.GetDcmtkObject());
-  }
-
-
-  StoreStatus ServerContext::Store(std::string& resultPublicId,
-                                   ParsedDicomFile& dicomInstance,
-                                   const char* dicomBuffer,
-                                   size_t dicomSize)
-  {
-    DicomMap dicomSummary;
-    FromDcmtkBridge::Convert(dicomSummary, *GetDicom(dicomInstance).getDataset());
-
-    try
-    {
-      DicomInstanceHasher hasher(dicomSummary);
-      resultPublicId = hasher.HashInstance();
-
-      Json::Value dicomJson;
-      FromDcmtkBridge::ToJson(dicomJson, *GetDicom(dicomInstance).getDataset());
-      
-      StoreStatus status = StoreStatus_Failure;
-      if (dicomSize > 0)
-      {
-        status = Store(dicomBuffer, dicomSize, dicomSummary, dicomJson, "");
-      }   
-
-      return status;
-    }
-    catch (OrthancException& e)
-    {
-      if (e.GetErrorCode() == ErrorCode_InexistentTag)
-      {
-        LogMissingRequiredTag(dicomSummary);
-      }
-
-      throw;
-    }
-  }
-
-
-  StoreStatus ServerContext::Store(std::string& resultPublicId,
-                                   ParsedDicomFile& dicomInstance)
-  {
-    std::string buffer;
-    if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, GetDicom(dicomInstance).getDataset()))
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (buffer.size() == 0)
-      return Store(resultPublicId, dicomInstance, NULL, 0);
-    else
-      return Store(resultPublicId, dicomInstance, &buffer[0], buffer.size());
-  }
-
-
-  StoreStatus ServerContext::Store(std::string& resultPublicId,
-                                   const char* dicomBuffer,
-                                   size_t dicomSize)
-  {
-    ParsedDicomFile dicom(dicomBuffer, dicomSize);
-    return Store(resultPublicId, dicom, dicomBuffer, dicomSize);
-  }
-
-
-  StoreStatus ServerContext::Store(std::string& resultPublicId,
-                                   const std::string& dicomContent)
-  {
-    if (dicomContent.size() == 0)
-    {
-      return Store(resultPublicId, NULL, 0);
-    }
-    else
-    {
-      return Store(resultPublicId, &dicomContent[0], dicomContent.size());
-    }
   }
 
 
@@ -358,7 +515,7 @@
 
     if (status != StoreStatus_Success)
     {
-      storage_.Remove(info.GetUuid());
+      accessor_.Remove(info.GetUuid(), info.GetContentType());
       return false;
     }
     else
@@ -366,4 +523,60 @@
       return true;
     }
   }
+
+
+  bool ServerContext::DeleteResource(Json::Value& target,
+                                     const std::string& uuid,
+                                     ResourceType expectedType)
+  {
+    return index_.DeleteResource(target, uuid, expectedType);
+  }
+
+
+  void ServerContext::SignalChange(const ServerIndexChange& change)
+  {
+    if (plugins_ != NULL)
+    {
+      try
+      {
+        plugins_->SignalChange(change);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Error in OnChangeCallback (plugins): " << e.What();
+      }
+    }
+  }
+
+
+  bool ServerContext::HasPlugins() const
+  {
+    return (pluginsManager_ && plugins_);
+  }
+
+
+  const PluginsManager& ServerContext::GetPluginsManager() const
+  {
+    if (HasPlugins())
+    {
+      return *pluginsManager_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  const OrthancPlugins& ServerContext::GetOrthancPlugins() const
+  {
+    if (HasPlugins())
+    {
+      return *plugins_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
 }
--- a/OrthancServer/ServerContext.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ServerContext.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -34,15 +34,23 @@
 
 #include "../Core/Cache/MemoryCache.h"
 #include "../Core/FileStorage/CompressedFileStorageAccessor.h"
-#include "../Core/FileStorage/FileStorage.h"
+#include "../Core/FileStorage/IStorageArea.h"
 #include "../Core/RestApi/RestApiOutput.h"
 #include "../Core/Lua/LuaContext.h"
 #include "ServerIndex.h"
 #include "ParsedDicomFile.h"
 #include "DicomProtocol/ReusableDicomUserConnection.h"
+#include "Scheduler/ServerScheduler.h"
+#include "DicomInstanceToStore.h"
+#include "ServerIndexChange.h"
+
+#include <boost/filesystem.hpp>
 
 namespace Orthanc
 {
+  class OrthancPlugins;
+  class PluginsManager;
+
   /**
    * This class is responsible for maintaining the storage area on the
    * filesystem (including compression), as well as the index of the
@@ -64,7 +72,15 @@
       virtual IDynamicObject* Provide(const std::string& id);
     };
 
-    FileStorage storage_;
+    bool ApplyReceivedInstanceFilter(const Json::Value& simplified,
+                                     const std::string& remoteAet);
+
+    void ApplyLuaOnStoredInstance(const std::string& instanceId,
+                                  const Json::Value& simplifiedDicom,
+                                  const Json::Value& metadata,
+                                  const std::string& remoteAet,
+                                  const std::string& calledAet);
+
     ServerIndex index_;
     CompressedFileStorageAccessor accessor_;
     bool compressionEnabled_;
@@ -73,15 +89,20 @@
     boost::mutex dicomCacheMutex_;
     MemoryCache dicomCache_;
     ReusableDicomUserConnection scu_;
+    ServerScheduler scheduler_;
 
+    boost::mutex luaMutex_;
     LuaContext lua_;
+    OrthancPlugins* plugins_;  // TODO Turn it into a listener pattern (idem for Lua callbacks)
+    const PluginsManager* pluginsManager_;
 
   public:
-    class DicomCacheLocker
+    class DicomCacheLocker : public boost::noncopyable
     {
     private:
       ServerContext& that_;
       ParsedDicomFile *dicom_;
+      boost::mutex::scoped_lock lock_;
 
     public:
       DicomCacheLocker(ServerContext& that,
@@ -95,8 +116,35 @@
       }
     };
 
-    ServerContext(const boost::filesystem::path& storagePath,
-                  const boost::filesystem::path& indexPath);
+    class LuaContextLocker : public boost::noncopyable
+    {
+    private:
+      ServerContext& that_;
+
+    public:
+      LuaContextLocker(ServerContext& that) : that_(that)
+      {
+        that.luaMutex_.lock();
+      }
+
+      ~LuaContextLocker()
+      {
+        that_.luaMutex_.unlock();
+      }
+
+      LuaContext& GetLua()
+      {
+        return that_.lua_;
+      }
+    };
+
+
+    ServerContext(IDatabaseWrapper& database);
+
+    void SetStorageArea(IStorageArea& storage)
+    {
+      accessor_.SetStorageArea(storage);
+    }
 
     ServerIndex& GetIndex()
     {
@@ -110,37 +158,20 @@
       return compressionEnabled_;
     }
 
-    void RemoveFile(const std::string& fileUuid);
+    void RemoveFile(const std::string& fileUuid,
+                    FileContentType type);
 
     bool AddAttachment(const std::string& resourceId,
                        FileContentType attachmentType,
                        const void* data,
                        size_t size);
 
-    StoreStatus Store(const char* dicomInstance,
-                      size_t dicomSize,
-                      const DicomMap& dicomSummary,
-                      const Json::Value& dicomJson,
-                      const std::string& remoteAet);
-
     StoreStatus Store(std::string& resultPublicId,
-                      ParsedDicomFile& dicomInstance,
-                      const char* dicomBuffer,
-                      size_t dicomSize);
+                      DicomInstanceToStore& dicom);
 
-    StoreStatus Store(std::string& resultPublicId,
-                      ParsedDicomFile& dicomInstance);
-
-    StoreStatus Store(std::string& resultPublicId,
-                      const char* dicomBuffer,
-                      size_t dicomSize);
-
-    StoreStatus Store(std::string& resultPublicId,
-                      const std::string& dicomContent);
-
-    void AnswerDicomFile(RestApiOutput& output,
-                         const std::string& instancePublicId,
-                         FileContentType content);
+    void AnswerAttachment(RestApiOutput& output,
+                          const std::string& instancePublicId,
+                          FileContentType content);
 
     void ReadJson(Json::Value& result,
                   const std::string& instancePublicId);
@@ -151,11 +182,6 @@
                   FileContentType content,
                   bool uncompressIfNeeded = true);
 
-    LuaContext& GetLuaContext()
-    {
-      return lua_;
-    }
-
     void SetStoreMD5ForAttachments(bool storeMD5);
 
     bool IsStoreMD5ForAttachments() const
@@ -167,5 +193,35 @@
     {
       return scu_;
     }
+
+    ServerScheduler& GetScheduler()
+    {
+      return scheduler_;
+    }
+
+    void SetOrthancPlugins(const PluginsManager& manager,
+                           OrthancPlugins& plugins)
+    {
+      pluginsManager_ = &manager;
+      plugins_ = &plugins;
+    }
+
+    void ResetOrthancPlugins()
+    {
+      pluginsManager_ = NULL;
+      plugins_ = NULL;
+    }
+
+    bool DeleteResource(Json::Value& target,
+                        const std::string& uuid,
+                        ResourceType expectedType);
+
+    void SignalChange(const ServerIndexChange& change);
+
+    bool HasPlugins() const;
+
+    const PluginsManager& GetPluginsManager() const;
+
+    const OrthancPlugins& GetOrthancPlugins() const;
   };
 }
--- a/OrthancServer/ServerEnumerations.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ServerEnumerations.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -48,6 +48,9 @@
   void InitializeServerEnumerations()
   {
     boost::mutex::scoped_lock lock(enumerationsMutex_);
+
+    dictMetadataType_.Clear();
+    dictContentType_.Clear();
     
     dictMetadataType_.Add(MetadataType_Instance_IndexInSeries, "IndexInSeries");
     dictMetadataType_.Add(MetadataType_Instance_ReceptionDate, "ReceptionDate");
@@ -228,6 +231,12 @@
       case ChangeType_StableSeries:
         return "StableSeries";
 
+      case ChangeType_Deleted:
+        return "Deleted";
+
+      case ChangeType_NewChildInstance:
+        return "NewChildInstance";
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -279,6 +288,9 @@
       case ModalityManufacturer_Generic:
         return "Generic";
 
+      case ModalityManufacturer_StoreScp:
+        return "StoreScp";
+      
       case ModalityManufacturer_ClearCanvas:
         return "ClearCanvas";
       
@@ -318,7 +330,6 @@
         return "Store";
         break;
 
-
       default: 
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -336,6 +347,10 @@
     {
       return ModalityManufacturer_ClearCanvas;
     }
+    else if (manufacturer == "StoreScp")
+    {
+      return ModalityManufacturer_StoreScp;
+    }
     else if (manufacturer == "MedInria")
     {
       return ModalityManufacturer_MedInria;
@@ -349,4 +364,36 @@
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
+
+
+  const char* EnumerationToString(TransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      case TransferSyntax_Deflated:
+        return "Deflated";
+
+      case TransferSyntax_Jpeg:
+        return "JPEG";
+
+      case TransferSyntax_Jpeg2000:
+        return "JPEG2000";
+
+      case TransferSyntax_JpegLossless:
+        return "JPEG Lossless";
+
+      case TransferSyntax_Jpip:
+        return "JPIP";
+
+      case TransferSyntax_Mpeg2:
+        return "MPEG2";
+
+      case TransferSyntax_Rle:
+        return "RLE";
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
 }
--- a/OrthancServer/ServerEnumerations.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ServerEnumerations.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -32,6 +32,7 @@
 #pragma once
 
 #include <string>
+#include <map>
 
 #include "../Core/Enumerations.h"
 
@@ -56,6 +57,7 @@
   enum ModalityManufacturer
   {
     ModalityManufacturer_Generic,
+    ModalityManufacturer_StoreScp,
     ModalityManufacturer_ClearCanvas,
     ModalityManufacturer_MedInria,
     ModalityManufacturer_Dcm4Chee
@@ -77,6 +79,17 @@
     DicomReplaceMode_IgnoreIfAbsent
   };
 
+  enum TransferSyntax
+  {
+    TransferSyntax_Deflated,
+    TransferSyntax_Jpeg,
+    TransferSyntax_Jpeg2000,
+    TransferSyntax_JpegLossless,
+    TransferSyntax_Jpip,
+    TransferSyntax_Mpeg2,
+    TransferSyntax_Rle
+  };
+
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -121,9 +134,17 @@
     ChangeType_ModifiedPatient = 11,
     ChangeType_StablePatient = 12,
     ChangeType_StableStudy = 13,
-    ChangeType_StableSeries = 14
+    ChangeType_StableSeries = 14,
+
+    ChangeType_INTERNAL_LastLogged = 4095,
+
+    // The changes below this point are not logged into the database
+    ChangeType_Deleted = 4096,
+    ChangeType_NewChildInstance = 4097
   };
 
+
+
   void InitializeServerEnumerations();
 
   void RegisterUserMetadata(int metadata,
@@ -153,6 +174,8 @@
 
   const char* EnumerationToString(DicomRequestType type);
 
+  const char* EnumerationToString(TransferSyntax syntax);
+
   ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer);
 
   ResourceType GetParentResourceType(ResourceType type);
--- a/OrthancServer/ServerIndex.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ServerIndex.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -37,12 +37,12 @@
 #define NOMINMAX
 #endif
 
+#include "ServerIndexChange.h"
 #include "EmbeddedResources.h"
 #include "OrthancInitialization.h"
 #include "../Core/Toolbox.h"
 #include "../Core/Uuid.h"
 #include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/SQLite/Transaction.h"
 #include "FromDcmtkBridge.h"
 #include "ServerContext.h"
 
@@ -59,16 +59,49 @@
     class ServerIndexListener : public IServerIndexListener
     {
     private:
+      struct FileToRemove
+      {
+      private:
+        std::string  uuid_;
+        FileContentType  type_;
+
+      public:
+        FileToRemove(const FileInfo& info) : uuid_(info.GetUuid()), 
+                                             type_(info.GetContentType())
+        {
+        }
+
+        const std::string& GetUuid() const
+        {
+          return uuid_;
+        }
+
+        FileContentType GetContentType() const 
+        {
+          return type_;
+        }
+      };
+
       ServerContext& context_;
       bool hasRemainingLevel_;
       ResourceType remainingType_;
       std::string remainingPublicId_;
-      std::list<std::string> pendingFilesToRemove_;
+      std::list<FileToRemove> pendingFilesToRemove_;
+      std::list<ServerIndexChange> pendingChanges_;
       uint64_t sizeOfFilesToRemove_;
+      bool insideTransaction_;
+
+      void Reset()
+      {
+        sizeOfFilesToRemove_ = 0;
+        hasRemainingLevel_ = false;
+        pendingFilesToRemove_.clear();
+        pendingChanges_.clear();
+      }
 
     public:
-      ServerIndexListener(ServerContext& context) : 
-        context_(context)
+      ServerIndexListener(ServerContext& context) : context_(context),
+                                                    insideTransaction_(false)      
       {
         Reset();
         assert(ResourceType_Patient < ResourceType_Study &&
@@ -76,11 +109,15 @@
                ResourceType_Series < ResourceType_Instance);
       }
 
-      void Reset()
+      void StartTransaction()
       {
-        sizeOfFilesToRemove_ = 0;
-        hasRemainingLevel_ = false;
-        pendingFilesToRemove_.clear();
+        Reset();
+        insideTransaction_ = true;
+      }
+
+      void EndTransaction()
+      {
+        insideTransaction_ = false;
       }
 
       uint64_t GetSizeOfFilesToRemove()
@@ -90,18 +127,28 @@
 
       void CommitFilesToRemove()
       {
-        for (std::list<std::string>::iterator 
+        for (std::list<FileToRemove>::const_iterator 
                it = pendingFilesToRemove_.begin();
              it != pendingFilesToRemove_.end(); ++it)
         {
-          context_.RemoveFile(*it);
+          context_.RemoveFile(it->GetUuid(), it->GetContentType());
+        }
+      }
+
+      void CommitChanges()
+      {
+        for (std::list<ServerIndexChange>::const_iterator 
+               it = pendingChanges_.begin(); 
+             it != pendingChanges_.end(); ++it)
+        {
+          context_.SignalChange(*it);
         }
       }
 
       virtual void SignalRemainingAncestor(ResourceType parentType,
                                            const std::string& publicId)
       {
-        LOG(INFO) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")";
+        VLOG(1) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")";
 
         if (hasRemainingLevel_)
         {
@@ -122,10 +169,26 @@
       virtual void SignalFileDeleted(const FileInfo& info)
       {
         assert(Toolbox::IsUuid(info.GetUuid()));
-        pendingFilesToRemove_.push_back(info.GetUuid());
+        pendingFilesToRemove_.push_back(FileToRemove(info));
         sizeOfFilesToRemove_ += info.GetCompressedSize();
       }
 
+      virtual void SignalChange(const ServerIndexChange& change)
+      {
+        VLOG(1) << "Change related to resource " << change.GetPublicId() << " of type " 
+                << EnumerationToString(change.GetResourceType()) << ": " 
+                << EnumerationToString(change.GetChangeType());
+
+        if (insideTransaction_)
+        {
+          pendingChanges_.push_back(change);
+        }
+        else
+        {
+          context_.SignalChange(change);
+        }
+      }
+
       bool HasRemainingLevel() const
       {
         return hasRemainingLevel_;
@@ -150,7 +213,7 @@
   {
   private:
     ServerIndex& index_;
-    std::auto_ptr<SQLite::Transaction> transaction_;
+    std::auto_ptr<SQLite::ITransaction> transaction_;
     bool isCommitted_;
 
   public:
@@ -158,11 +221,22 @@
       index_(index),
       isCommitted_(false)
     {
-      assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize());
+      transaction_.reset(index_.db_.StartTransaction());
+      transaction_->Begin();
+
+      assert(index_.currentStorageSize_ == index_.db_.GetTotalCompressedSize());
+
+      index_.listener_->StartTransaction();
+    }
 
-      index_.listener_->Reset();
-      transaction_.reset(index_.db_->StartTransaction());
-      transaction_->Begin();
+    ~Transaction()
+    {
+      index_.listener_->EndTransaction();
+
+      if (!isCommitted_)
+      {
+        transaction_->Rollback();
+      }
     }
 
     void Commit(uint64_t sizeOfAddedFiles)
@@ -181,7 +255,8 @@
         assert(index_.currentStorageSize_ >= index_.listener_->GetSizeOfFilesToRemove());
         index_.currentStorageSize_ -= index_.listener_->GetSizeOfFilesToRemove();
 
-        assert(index_.currentStorageSize_ == index_.db_->GetTotalCompressedSize());
+        // Send all the pending changes to the Orthanc plugins
+        index_.listener_->CommitChanges();
 
         isCommitted_ = true;
       }
@@ -189,16 +264,22 @@
   };
 
 
-  struct ServerIndex::UnstableResourcePayload
+  class ServerIndex::UnstableResourcePayload
   {
-    Orthanc::ResourceType type_;
+  private:
+    ResourceType type_;
+    std::string publicId_;
     boost::posix_time::ptime time_;
 
-    UnstableResourcePayload() : type_(Orthanc::ResourceType_Instance)
+  public:
+    UnstableResourcePayload() : type_(ResourceType_Instance)
     {
     }
 
-    UnstableResourcePayload(Orthanc::ResourceType type) : type_(type)
+    UnstableResourcePayload(Orthanc::ResourceType type,
+                            const std::string& publicId) : 
+      type_(type),
+      publicId_(publicId)
     {
       time_ = boost::posix_time::second_clock::local_time();
     }
@@ -207,6 +288,16 @@
     {
       return (boost::posix_time::second_clock::local_time() - time_).total_seconds();
     }
+
+    ResourceType GetResourceType() const
+    {
+      return type_;
+    }
+    
+    const std::string& GetPublicId() const
+    {
+      return publicId_;
+    }
   };
 
 
@@ -215,19 +306,18 @@
                                    ResourceType expectedType)
   {
     boost::mutex::scoped_lock lock(mutex_);
-    listener_->Reset();
 
     Transaction t(*this);
 
     int64_t id;
     ResourceType type;
-    if (!db_->LookupResource(uuid, id, type) ||
+    if (!db_.LookupResource(id, type, uuid) ||
         expectedType != type)
     {
       return false;
     }
       
-    db_->DeleteResource(id);
+    db_.DeleteResource(id);
 
     if (listener_->HasRemainingLevel())
     {
@@ -252,18 +342,22 @@
 
   void ServerIndex::FlushThread(ServerIndex* that)
   {
-    unsigned int sleep;
+    // By default, wait for 10 seconds before flushing
+    unsigned int sleep = 10;
 
     try
     {
       boost::mutex::scoped_lock lock(that->mutex_);
-      std::string sleepString = that->db_->GetGlobalProperty(GlobalProperty_FlushSleep);
-      sleep = boost::lexical_cast<unsigned int>(sleepString);
+      std::string sleepString;
+
+      if (that->db_.LookupGlobalProperty(sleepString, GlobalProperty_FlushSleep) &&
+          Toolbox::IsInteger(sleepString))
+      {
+        sleep = boost::lexical_cast<unsigned int>(sleepString);
+      }
     }
     catch (boost::bad_lexical_cast&)
     {
-      // By default, wait for 10 seconds before flushing
-      sleep = 10;
     }
 
     LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")";
@@ -280,7 +374,7 @@
       }
 
       boost::mutex::scoped_lock lock(that->mutex_);
-      that->db_->FlushToDisk();
+      that->db_.FlushToDisk();
       count = 0;
     }
 
@@ -288,7 +382,7 @@
   }
 
 
-  static void ComputeExpectedNumberOfInstances(DatabaseWrapper& db,
+  static void ComputeExpectedNumberOfInstances(IDatabaseWrapper& db,
                                                int64_t series,
                                                const DicomMap& dicomSummary)
   {
@@ -328,40 +422,147 @@
   }
 
 
+
+
+  bool ServerIndex::GetMetadataAsInteger(int64_t& result,
+                                         int64_t id,
+                                         MetadataType type)
+  {
+    std::string s;
+    if (!db_.LookupMetadata(s, id, type))
+    {
+      return false;
+    }
+
+    try
+    {
+      result = boost::lexical_cast<int64_t>(s);
+      return true;
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return false;
+    }
+  }
+
+
+  void ServerIndex::LogChange(int64_t internalId,
+                              ChangeType changeType,
+                              ResourceType resourceType,
+                              const std::string& publicId)
+  {
+    ServerIndexChange change(changeType, resourceType, publicId);
+
+    if (changeType <= ChangeType_INTERNAL_LastLogged)
+    {
+      db_.LogChange(internalId, change);
+    }
+
+    assert(listener_.get() != NULL);
+    listener_->SignalChange(change);
+  }
+
+
+  uint64_t ServerIndex::IncrementGlobalSequenceInternal(GlobalProperty property)
+  {
+    std::string oldValue;
+
+    if (db_.LookupGlobalProperty(oldValue, property))
+    {
+      uint64_t oldNumber;
+
+      try
+      {
+        oldNumber = boost::lexical_cast<uint64_t>(oldValue);
+        db_.SetGlobalProperty(property, boost::lexical_cast<std::string>(oldNumber + 1));
+        return oldNumber + 1;
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      // Initialize the sequence at "1"
+      db_.SetGlobalProperty(property, "1");
+      return 1;
+    }
+  }
+
+
+
+  void ServerIndex::SetMainDicomTags(int64_t resource,
+                                     const DicomMap& tags)
+  {
+    DicomArray flattened(tags);
+    for (size_t i = 0; i < flattened.GetSize(); i++)
+    {
+      const DicomElement& element = flattened.GetElement(i);
+      db_.SetMainDicomTag(resource, element.GetTag(), element.GetValue().AsString());
+    }
+  }
+
+
+  int64_t ServerIndex::CreateResource(const std::string& publicId,
+                                      ResourceType type)
+  {
+    int64_t id = db_.CreateResource(publicId, type);
+
+    ChangeType changeType;
+    switch (type)
+    {
+    case ResourceType_Patient: 
+      changeType = ChangeType_NewPatient; 
+      break;
+
+    case ResourceType_Study: 
+      changeType = ChangeType_NewStudy; 
+      break;
+
+    case ResourceType_Series: 
+      changeType = ChangeType_NewSeries; 
+      break;
+
+    case ResourceType_Instance: 
+      changeType = ChangeType_NewInstance; 
+      break;
+
+    default:
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    ServerIndexChange change(changeType, type, publicId);
+    db_.LogChange(id, change);
+
+    assert(listener_.get() != NULL);
+    listener_->SignalChange(change);
+
+    return id;
+  }
+
+
   ServerIndex::ServerIndex(ServerContext& context,
-                           const std::string& dbPath) : 
+                           IDatabaseWrapper& db) : 
     done_(false),
+    db_(db),
     maximumStorageSize_(0),
     maximumPatients_(0)
   {
     listener_.reset(new Internals::ServerIndexListener(context));
-
-    if (dbPath == ":memory:")
-    {
-      db_.reset(new DatabaseWrapper(*listener_));
-    }
-    else
-    {
-      boost::filesystem::path p = dbPath;
+    db_.SetListener(*listener_);
 
-      try
-      {
-        boost::filesystem::create_directories(p);
-      }
-      catch (boost::filesystem::filesystem_error)
-      {
-      }
-
-      db_.reset(new DatabaseWrapper(p.string() + "/index", *listener_));
-    }
-
-    currentStorageSize_ = db_->GetTotalCompressedSize();
+    currentStorageSize_ = db_.GetTotalCompressedSize();
 
     // Initial recycling if the parameters have changed since the last
     // execution of Orthanc
     StandaloneRecycling();
 
-    flushThread_ = boost::thread(FlushThread, this);
+    if (db.HasFlushToDisk())
+    {
+      flushThread_ = boost::thread(FlushThread, this);
+    }
+
     unstableResourcesMonitorThread_ = boost::thread(UnstableResourcesMonitorThread, this);
   }
 
@@ -370,7 +571,8 @@
   {
     done_ = true;
 
-    if (flushThread_.joinable())
+    if (db_.HasFlushToDisk() &&
+        flushThread_.joinable())
     {
       flushThread_.join();
     }
@@ -382,12 +584,15 @@
   }
 
 
-  StoreStatus ServerIndex::Store(const DicomMap& dicomSummary,
+  StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
+                                 const DicomMap& dicomSummary,
                                  const Attachments& attachments,
-                                 const std::string& remoteAet)
+                                 const std::string& remoteAet,
+                                 const MetadataMap& metadata)
   {
     boost::mutex::scoped_lock lock(mutex_);
-    listener_->Reset();
+
+    instanceMetadata.clear();
 
     DicomInstanceHasher hasher(dicomSummary);
 
@@ -399,9 +604,10 @@
       {
         ResourceType type;
         int64_t tmp;
-        if (db_->LookupResource(hasher.HashInstance(), tmp, type))
+        if (db_.LookupResource(tmp, type, hasher.HashInstance()))
         {
           assert(type == ResourceType_Instance);
+          db_.GetAllMetadata(instanceMetadata, tmp);
           return StoreStatus_AlreadyStored;
         }
       }
@@ -417,11 +623,11 @@
       Recycle(instanceSize, hasher.HashPatient());
 
       // Create the instance
-      int64_t instance = db_->CreateResource(hasher.HashInstance(), ResourceType_Instance);
+      int64_t instance = CreateResource(hasher.HashInstance(), ResourceType_Instance);
 
       DicomMap dicom;
       dicomSummary.ExtractInstanceInformation(dicom);
-      db_->SetMainDicomTags(instance, dicom);
+      SetMainDicomTags(instance, dicom);
 
       // Detect up to which level the patient/study/series/instance
       // hierarchy must be created
@@ -433,26 +639,26 @@
       {
         ResourceType dummy;
 
-        if (db_->LookupResource(hasher.HashSeries(), series, dummy))
+        if (db_.LookupResource(series, dummy, hasher.HashSeries()))
         {
           assert(dummy == ResourceType_Series);
           // The patient, the study and the series already exist
 
-          bool ok = (db_->LookupResource(hasher.HashPatient(), patient, dummy) &&
-                     db_->LookupResource(hasher.HashStudy(), study, dummy));
+          bool ok = (db_.LookupResource(patient, dummy, hasher.HashPatient()) &&
+                     db_.LookupResource(study, dummy, hasher.HashStudy()));
           assert(ok);
         }
-        else if (db_->LookupResource(hasher.HashStudy(), study, dummy))
+        else if (db_.LookupResource(study, dummy, hasher.HashStudy()))
         {
           assert(dummy == ResourceType_Study);
 
           // New series: The patient and the study already exist
           isNewSeries = true;
 
-          bool ok = db_->LookupResource(hasher.HashPatient(), patient, dummy);
+          bool ok = db_.LookupResource(patient, dummy, hasher.HashPatient());
           assert(ok);
         }
-        else if (db_->LookupResource(hasher.HashPatient(), patient, dummy))
+        else if (db_.LookupResource(patient, dummy, hasher.HashPatient()))
         {
           assert(dummy == ResourceType_Patient);
 
@@ -472,38 +678,38 @@
       // Create the series if needed
       if (isNewSeries)
       {
-        series = db_->CreateResource(hasher.HashSeries(), ResourceType_Series);
+        series = CreateResource(hasher.HashSeries(), ResourceType_Series);
         dicomSummary.ExtractSeriesInformation(dicom);
-        db_->SetMainDicomTags(series, dicom);
+        SetMainDicomTags(series, dicom);
       }
 
       // Create the study if needed
       if (isNewStudy)
       {
-        study = db_->CreateResource(hasher.HashStudy(), ResourceType_Study);
+        study = CreateResource(hasher.HashStudy(), ResourceType_Study);
         dicomSummary.ExtractStudyInformation(dicom);
-        db_->SetMainDicomTags(study, dicom);
+        SetMainDicomTags(study, dicom);
       }
 
       // Create the patient if needed
       if (isNewPatient)
       {
-        patient = db_->CreateResource(hasher.HashPatient(), ResourceType_Patient);
+        patient = CreateResource(hasher.HashPatient(), ResourceType_Patient);
         dicomSummary.ExtractPatientInformation(dicom);
-        db_->SetMainDicomTags(patient, dicom);
+        SetMainDicomTags(patient, dicom);
       }
 
       // Create the parent-to-child links
-      db_->AttachChild(series, instance);
+      db_.AttachChild(series, instance);
 
       if (isNewSeries)
       {
-        db_->AttachChild(study, series);
+        db_.AttachChild(study, series);
       }
 
       if (isNewStudy)
       {
-        db_->AttachChild(patient, study);
+        db_.AttachChild(patient, study);
       }
 
       // Sanity checks
@@ -516,40 +722,75 @@
       for (Attachments::const_iterator it = attachments.begin();
            it != attachments.end(); ++it)
       {
-        db_->AddAttachment(instance, *it);
+        db_.AddAttachment(instance, *it);
       }
 
-      // Attach the metadata
+      // Attach the user-specified metadata
+      for (MetadataMap::const_iterator 
+             it = metadata.begin(); it != metadata.end(); ++it)
+      {
+        switch (it->first.first)
+        {
+          case ResourceType_Patient:
+            db_.SetMetadata(patient, it->first.second, it->second);
+            break;
+
+          case ResourceType_Study:
+            db_.SetMetadata(study, it->first.second, it->second);
+            break;
+
+          case ResourceType_Series:
+            db_.SetMetadata(series, it->first.second, it->second);
+            break;
+
+          case ResourceType_Instance:
+            db_.SetMetadata(instance, it->first.second, it->second);
+            instanceMetadata[it->first.second] = it->second;
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+      }
+
+      // Attach the auto-computed metadata for the patient/study/series levels
       std::string now = Toolbox::GetNowIsoString();
-      db_->SetMetadata(instance, MetadataType_Instance_ReceptionDate, now);
-      db_->SetMetadata(series, MetadataType_LastUpdate, now);
-      db_->SetMetadata(study, MetadataType_LastUpdate, now);
-      db_->SetMetadata(patient, MetadataType_LastUpdate, now);
-      db_->SetMetadata(instance, MetadataType_Instance_RemoteAet, remoteAet);
+      db_.SetMetadata(series, MetadataType_LastUpdate, now);
+      db_.SetMetadata(study, MetadataType_LastUpdate, now);
+      db_.SetMetadata(patient, MetadataType_LastUpdate, now);
+
+      // Attach the auto-computed metadata for the instance level,
+      // reflecting these additions into the input metadata map
+      db_.SetMetadata(instance, MetadataType_Instance_ReceptionDate, now);
+      instanceMetadata[MetadataType_Instance_ReceptionDate] = now;
+
+      db_.SetMetadata(instance, MetadataType_Instance_RemoteAet, remoteAet);
+      instanceMetadata[MetadataType_Instance_RemoteAet] = remoteAet;
 
       const DicomValue* value;
       if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
           (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
       {
-        db_->SetMetadata(instance, MetadataType_Instance_IndexInSeries, value->AsString());
-      }
-
-      if (isNewSeries)
-      {
-        ComputeExpectedNumberOfInstances(*db_, series, dicomSummary);
+        db_.SetMetadata(instance, MetadataType_Instance_IndexInSeries, value->AsString());
+        instanceMetadata[MetadataType_Instance_IndexInSeries] = value->AsString();
       }
 
       // Check whether the series of this new instance is now completed
+      if (isNewSeries)
+      {
+        ComputeExpectedNumberOfInstances(db_, series, dicomSummary);
+      }
+
       SeriesStatus seriesStatus = GetSeriesStatus(series);
       if (seriesStatus == SeriesStatus_Complete)
       {
-        db_->LogChange(ChangeType_CompletedSeries, series, ResourceType_Series);
+        LogChange(series, ChangeType_CompletedSeries, ResourceType_Series, hasher.HashSeries());
       }
 
       // Mark the parent resources of this instance as unstable
-      MarkAsUnstable(patient, ResourceType_Patient);
-      MarkAsUnstable(study, ResourceType_Study);
-      MarkAsUnstable(series, ResourceType_Series);
+      MarkAsUnstable(series, ResourceType_Series, hasher.HashSeries());
+      MarkAsUnstable(study, ResourceType_Study, hasher.HashStudy());
+      MarkAsUnstable(patient, ResourceType_Patient, hasher.HashPatient());
 
       t.Commit(instanceSize);
 
@@ -557,8 +798,7 @@
     }
     catch (OrthancException& e)
     {
-      LOG(ERROR) << "EXCEPTION [" << e.What() << "]" 
-                 << " (SQLite status: " << db_->GetErrorMessage() << ")";
+      LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
     }
 
     return StoreStatus_Failure;
@@ -571,17 +811,17 @@
     target = Json::objectValue;
 
     uint64_t cs = currentStorageSize_;
-    assert(cs == db_->GetTotalCompressedSize());
-    uint64_t us = db_->GetTotalUncompressedSize();
+    assert(cs == db_.GetTotalCompressedSize());
+    uint64_t us = db_.GetTotalUncompressedSize();
     target["TotalDiskSize"] = boost::lexical_cast<std::string>(cs);
     target["TotalUncompressedSize"] = boost::lexical_cast<std::string>(us);
-    target["TotalDiskSizeMB"] = boost::lexical_cast<unsigned int>(cs / MEGA_BYTES);
-    target["TotalUncompressedSizeMB"] = boost::lexical_cast<unsigned int>(us / MEGA_BYTES);
+    target["TotalDiskSizeMB"] = static_cast<unsigned int>(cs / MEGA_BYTES);
+    target["TotalUncompressedSizeMB"] = static_cast<unsigned int>(us / MEGA_BYTES);
 
-    target["CountPatients"] = static_cast<unsigned int>(db_->GetResourceCount(ResourceType_Patient));
-    target["CountStudies"] = static_cast<unsigned int>(db_->GetResourceCount(ResourceType_Study));
-    target["CountSeries"] = static_cast<unsigned int>(db_->GetResourceCount(ResourceType_Series));
-    target["CountInstances"] = static_cast<unsigned int>(db_->GetResourceCount(ResourceType_Instance));
+    target["CountPatients"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Patient));
+    target["CountStudies"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Study));
+    target["CountSeries"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Series));
+    target["CountInstances"] = static_cast<unsigned int>(db_.GetResourceCount(ResourceType_Instance));
   }          
 
 
@@ -589,34 +829,23 @@
   SeriesStatus ServerIndex::GetSeriesStatus(int64_t id)
   {
     // Get the expected number of instances in this series (from the metadata)
-    std::string s = db_->GetMetadata(id, MetadataType_Series_ExpectedNumberOfInstances);
-
-    size_t expected;
-    try
-    {
-      expected = boost::lexical_cast<size_t>(s);
-    }
-    catch (boost::bad_lexical_cast&)
+    int64_t expected;
+    if (!GetMetadataAsInteger(expected, id, MetadataType_Series_ExpectedNumberOfInstances))
     {
       return SeriesStatus_Unknown;
     }
 
     // Loop over the instances of this series
     std::list<int64_t> children;
-    db_->GetChildrenInternalId(children, id);
+    db_.GetChildrenInternalId(children, id);
 
-    std::set<size_t> instances;
+    std::set<int64_t> instances;
     for (std::list<int64_t>::const_iterator 
            it = children.begin(); it != children.end(); ++it)
     {
       // Get the index of this instance in the series
-      s = db_->GetMetadata(*it, MetadataType_Instance_IndexInSeries);
-      size_t index;
-      try
-      {
-        index = boost::lexical_cast<size_t>(s);
-      }
-      catch (boost::bad_lexical_cast&)
+      int64_t index;
+      if (!GetMetadataAsInteger(index, *it, MetadataType_Instance_IndexInSeries))
       {
         return SeriesStatus_Unknown;
       }
@@ -636,7 +865,7 @@
       instances.insert(index);
     }
 
-    if (instances.size() == expected)
+    if (static_cast<int64_t>(instances.size()) == expected)
     {
       return SeriesStatus_Complete;
     }
@@ -652,7 +881,7 @@
                                         int64_t resourceId)
   {
     DicomMap tags;
-    db_->GetMainDicomTags(tags, resourceId);
+    db_.GetMainDicomTags(tags, resourceId);
     target["MainDicomTags"] = Json::objectValue;
     FromDcmtkBridge::ToJson(target["MainDicomTags"], tags);
   }
@@ -668,7 +897,7 @@
     // Lookup for the requested resource
     int64_t id;
     ResourceType type;
-    if (!db_->LookupResource(publicId, id, type) ||
+    if (!db_.LookupResource(id, type, publicId) ||
         type != expectedType)
     {
       return false;
@@ -678,12 +907,12 @@
     if (type != ResourceType_Patient)
     {
       int64_t parentId;
-      if (!db_->LookupParent(parentId, id))
+      if (!db_.LookupParent(parentId, id))
       {
         throw OrthancException(ErrorCode_InternalError);
       }
 
-      std::string parent = db_->GetPublicId(parentId);
+      std::string parent = db_.GetPublicId(parentId);
 
       switch (type)
       {
@@ -706,7 +935,7 @@
 
     // List the children resources
     std::list<std::string> children;
-    db_->GetChildrenPublicId(children, id);
+    db_.GetChildrenPublicId(children, id);
 
     if (type != ResourceType_Instance)
     {
@@ -753,9 +982,9 @@
         result["Type"] = "Series";
         result["Status"] = EnumerationToString(GetSeriesStatus(id));
 
-        int i;
-        if (db_->GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances))
-          result["ExpectedNumberOfInstances"] = i;
+        int64_t i;
+        if (GetMetadataAsInteger(i, id, MetadataType_Series_ExpectedNumberOfInstances))
+          result["ExpectedNumberOfInstances"] = static_cast<int>(i);
         else
           result["ExpectedNumberOfInstances"] = Json::nullValue;
 
@@ -767,7 +996,7 @@
         result["Type"] = "Instance";
 
         FileInfo attachment;
-        if (!db_->LookupAttachment(attachment, id, FileContentType_Dicom))
+        if (!db_.LookupAttachment(attachment, id, FileContentType_Dicom))
         {
           throw OrthancException(ErrorCode_InternalError);
         }
@@ -775,9 +1004,9 @@
         result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize());
         result["FileUuid"] = attachment.GetUuid();
 
-        int i;
-        if (db_->GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries))
-          result["IndexInSeries"] = i;
+        int64_t i;
+        if (GetMetadataAsInteger(i, id, MetadataType_Instance_IndexInSeries))
+          result["IndexInSeries"] = static_cast<int>(i);
         else
           result["IndexInSeries"] = Json::nullValue;
 
@@ -794,19 +1023,26 @@
 
     std::string tmp;
 
-    tmp = db_->GetMetadata(id, MetadataType_AnonymizedFrom);
-    if (tmp.size() != 0)
+    if (db_.LookupMetadata(tmp, id, MetadataType_AnonymizedFrom))
+    {
       result["AnonymizedFrom"] = tmp;
+    }
 
-    tmp = db_->GetMetadata(id, MetadataType_ModifiedFrom);
-    if (tmp.size() != 0)
+    if (db_.LookupMetadata(tmp, id, MetadataType_ModifiedFrom))
+    {
       result["ModifiedFrom"] = tmp;
+    }
 
     if (type == ResourceType_Patient ||
         type == ResourceType_Study ||
         type == ResourceType_Series)
     {
       result["IsStable"] = !unstableResources_.Contains(id);
+
+      if (db_.LookupMetadata(tmp, id, MetadataType_LastUpdate))
+      {
+        result["LastUpdate"] = tmp;
+      }
     }
 
     return true;
@@ -821,12 +1057,12 @@
 
     int64_t id;
     ResourceType type;
-    if (!db_->LookupResource(instanceUuid, id, type))
+    if (!db_.LookupResource(id, type, instanceUuid))
     {
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    if (db_->LookupAttachment(attachment, id, contentType))
+    if (db_.LookupAttachment(attachment, id, contentType))
     {
       assert(attachment.GetContentType() == contentType);
       return true;
@@ -839,38 +1075,77 @@
 
 
 
-  void ServerIndex::GetAllUuids(Json::Value& target,
+  void ServerIndex::GetAllUuids(std::list<std::string>& target,
                                 ResourceType resourceType)
   {
     boost::mutex::scoped_lock lock(mutex_);
-    db_->GetAllPublicIds(target, resourceType);
+    db_.GetAllPublicIds(target, resourceType);
   }
 
 
-  bool ServerIndex::GetChanges(Json::Value& target,
+  template <typename T>
+  static void FormatLog(Json::Value& target,
+                        const std::list<T>& log,
+                        const std::string& name,
+                        bool done,
+                        int64_t since)
+  {
+    Json::Value items = Json::arrayValue;
+    for (typename std::list<T>::const_iterator
+           it = log.begin(); it != log.end(); ++it)
+    {
+      Json::Value item;
+      it->Format(item);
+      items.append(item);
+    }
+
+    target = Json::objectValue;
+    target[name] = items;
+    target["Done"] = done;
+
+    int64_t last = (log.empty() ? since : log.back().GetSeq());
+    target["Last"] = static_cast<int>(last);
+  }
+
+
+  void ServerIndex::GetChanges(Json::Value& target,
                                int64_t since,                               
                                unsigned int maxResults)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-    db_->GetChanges(target, since, maxResults);
-    return true;
+    std::list<ServerIndexChange> changes;
+    bool done;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.GetChanges(changes, done, since, maxResults);
+    }
+
+    FormatLog(target, changes, "Changes", done, since);
   }
 
-  bool ServerIndex::GetLastChange(Json::Value& target)
+
+  void ServerIndex::GetLastChange(Json::Value& target)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-    db_->GetLastChange(target);
-    return true;
+    std::list<ServerIndexChange> changes;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.GetLastChange(changes);
+    }
+
+    FormatLog(target, changes, "Changes", true, 0);
   }
 
+
   void ServerIndex::LogExportedResource(const std::string& publicId,
                                         const std::string& remoteModality)
   {
     boost::mutex::scoped_lock lock(mutex_);
+    Transaction transaction(*this);
 
     int64_t id;
     ResourceType type;
-    if (!db_->LookupResource(publicId, id, type))
+    if (!db_.LookupResource(id, type, publicId))
     {
       throw OrthancException(ErrorCode_InternalError);
     }
@@ -888,7 +1163,7 @@
     while (!done)
     {
       DicomMap map;
-      db_->GetMainDicomTags(map, currentId);
+      db_.GetMainDicomTags(map, currentId);
 
       switch (currentType)
       {
@@ -920,36 +1195,52 @@
       // the current resource
       if (!done)
       {
-        bool ok = db_->LookupParent(currentId, currentId);
+        bool ok = db_.LookupParent(currentId, currentId);
         assert(ok);
       }
     }
 
-    // No need for a SQLite::Transaction here, as we only insert 1 record
-    db_->LogExportedResource(type,
-                             publicId,
-                             remoteModality,
-                             patientId,
-                             studyInstanceUid,
-                             seriesInstanceUid,
-                             sopInstanceUid);
+    ExportedResource resource(-1, 
+                              type,
+                              publicId,
+                              remoteModality,
+                              Toolbox::GetNowIsoString(),
+                              patientId,
+                              studyInstanceUid,
+                              seriesInstanceUid,
+                              sopInstanceUid);
+
+    db_.LogExportedResource(resource);
+    transaction.Commit(0);
   }
 
 
-  bool ServerIndex::GetExportedResources(Json::Value& target,
+  void ServerIndex::GetExportedResources(Json::Value& target,
                                          int64_t since,
                                          unsigned int maxResults)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-    db_->GetExportedResources(target, since, maxResults);
-    return true;
+    std::list<ExportedResource> exported;
+    bool done;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.GetExportedResources(exported, done, since, maxResults);
+    }
+
+    FormatLog(target, exported, "Exports", done, since);
   }
 
-  bool ServerIndex::GetLastExportedResource(Json::Value& target)
+
+  void ServerIndex::GetLastExportedResource(Json::Value& target)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-    db_->GetLastExportedResource(target);
-    return true;
+    std::list<ExportedResource> exported;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.GetLastExportedResource(exported);
+    }
+
+    FormatLog(target, exported, "Exports", true, 0);
   }
 
 
@@ -958,7 +1249,7 @@
     if (maximumStorageSize_ != 0)
     {
       uint64_t currentSize = currentStorageSize_ - listener_->GetSizeOfFilesToRemove();
-      assert(db_->GetTotalCompressedSize() == currentSize);
+      assert(db_.GetTotalCompressedSize() == currentSize);
 
       if (currentSize + instanceSize > maximumStorageSize_)
       {
@@ -968,7 +1259,7 @@
 
     if (maximumPatients_ != 0)
     {
-      uint64_t patientCount = db_->GetResourceCount(ResourceType_Patient);
+      uint64_t patientCount = db_.GetResourceCount(ResourceType_Patient);
       if (patientCount > maximumPatients_)
       {
         return true;
@@ -991,7 +1282,7 @@
     // already stored
     int64_t patientToAvoid;
     ResourceType type;
-    bool hasPatientToAvoid = db_->LookupResource(newPatientId, patientToAvoid, type);
+    bool hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId);
 
     if (hasPatientToAvoid && type != ResourceType_Patient)
     {
@@ -1006,16 +1297,16 @@
       // If other instances of this patient are already in the store,
       // we must avoid to recycle them
       bool ok = hasPatientToAvoid ?
-        db_->SelectPatientToRecycle(patientToRecycle, patientToAvoid) :
-        db_->SelectPatientToRecycle(patientToRecycle);
+        db_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) :
+        db_.SelectPatientToRecycle(patientToRecycle);
         
       if (!ok)
       {
         throw OrthancException(ErrorCode_FullStorage);
       }
       
-      LOG(INFO) << "Recycling one patient";
-      db_->DeleteResource(patientToRecycle);
+      VLOG(1) << "Recycling one patient";
+      db_.DeleteResource(patientToRecycle);
 
       if (!IsRecyclingNeeded(instanceSize))
       {
@@ -1075,13 +1366,13 @@
     // Lookup for the requested resource
     int64_t id;
     ResourceType type;
-    if (!db_->LookupResource(publicId, id, type) ||
+    if (!db_.LookupResource(id, type, publicId) ||
         type != ResourceType_Patient)
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    return db_->IsProtectedPatient(id);
+    return db_.IsProtectedPatient(id);
   }
      
 
@@ -1089,18 +1380,19 @@
                                         bool isProtected)
   {
     boost::mutex::scoped_lock lock(mutex_);
+    Transaction transaction(*this);
 
     // Lookup for the requested resource
     int64_t id;
     ResourceType type;
-    if (!db_->LookupResource(publicId, id, type) ||
+    if (!db_.LookupResource(id, type, publicId) ||
         type != ResourceType_Patient)
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
 
-    // No need for a SQLite::Transaction here, as we only make 1 write to the DB
-    db_->SetProtectedPatient(id, isProtected);
+    db_.SetProtectedPatient(id, isProtected);
+    transaction.Commit(0);
 
     if (isProtected)
       LOG(INFO) << "Patient " << publicId << " has been protected";
@@ -1118,7 +1410,7 @@
 
     ResourceType type;
     int64_t resource;
-    if (!db_->LookupResource(publicId, resource, type))
+    if (!db_.LookupResource(resource, type, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
@@ -1130,12 +1422,12 @@
     }
 
     std::list<int64_t> tmp;
-    db_->GetChildrenInternalId(tmp, resource);
+    db_.GetChildrenInternalId(tmp, resource);
 
     for (std::list<int64_t>::const_iterator 
            it = tmp.begin(); it != tmp.end(); ++it)
     {
-      result.push_back(db_->GetPublicId(*it));
+      result.push_back(db_.GetPublicId(*it));
     }
   }
 
@@ -1149,7 +1441,7 @@
 
     ResourceType type;
     int64_t top;
-    if (!db_->LookupResource(publicId, top, type))
+    if (!db_.LookupResource(top, type, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
@@ -1172,14 +1464,14 @@
       int64_t resource = toExplore.top();
       toExplore.pop();
 
-      if (db_->GetResourceType(resource) == ResourceType_Instance)
+      if (db_.GetResourceType(resource) == ResourceType_Instance)
       {
-        result.push_back(db_->GetPublicId(resource));
+        result.push_back(db_.GetPublicId(resource));
       }
       else
       {
         // Tag all the children of this resource as to be explored
-        db_->GetChildrenInternalId(tmp, resource);
+        db_.GetChildrenInternalId(tmp, resource);
         for (std::list<int64_t>::const_iterator 
                it = tmp.begin(); it != tmp.end(); ++it)
         {
@@ -1198,12 +1490,12 @@
 
     ResourceType rtype;
     int64_t id;
-    if (!db_->LookupResource(publicId, id, rtype))
+    if (!db_.LookupResource(id, rtype, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    db_->SetMetadata(id, type, value);
+    db_.SetMetadata(id, type, value);
   }
 
 
@@ -1214,12 +1506,12 @@
 
     ResourceType rtype;
     int64_t id;
-    if (!db_->LookupResource(publicId, id, rtype))
+    if (!db_.LookupResource(id, rtype, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    db_->DeleteMetadata(id, type);
+    db_.DeleteMetadata(id, type);
   }
 
 
@@ -1231,12 +1523,12 @@
 
     ResourceType rtype;
     int64_t id;
-    if (!db_->LookupResource(publicId, id, rtype))
+    if (!db_.LookupResource(id, rtype, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    return db_->LookupMetadata(target, id, type);
+    return db_.LookupMetadata(target, id, type);
   }
 
 
@@ -1247,12 +1539,12 @@
 
     ResourceType rtype;
     int64_t id;
-    if (!db_->LookupResource(publicId, id, rtype))
+    if (!db_.LookupResource(id, rtype, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    db_->ListAvailableMetadata(target, id);
+    db_.ListAvailableMetadata(target, id);
   }
 
 
@@ -1264,13 +1556,13 @@
 
     ResourceType type;
     int64_t id;
-    if (!db_->LookupResource(publicId, id, type) ||
+    if (!db_.LookupResource(id, type, publicId) ||
         expectedType != type)
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    db_->ListAvailableAttachments(target, id);
+    db_.ListAvailableAttachments(target, id);
   }
 
 
@@ -1281,15 +1573,15 @@
 
     ResourceType type;
     int64_t id;
-    if (!db_->LookupResource(publicId, id, type))
+    if (!db_.LookupResource(id, type, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
     int64_t parentId;
-    if (db_->LookupParent(parentId, id))
+    if (db_.LookupParent(parentId, id))
     {
-      target = db_->GetPublicId(parentId);
+      target = db_.GetPublicId(parentId);
       return true;
     }
     else
@@ -1302,12 +1594,10 @@
   uint64_t ServerIndex::IncrementGlobalSequence(GlobalProperty sequence)
   {
     boost::mutex::scoped_lock lock(mutex_);
-
-    std::auto_ptr<SQLite::Transaction> transaction(db_->StartTransaction());
+    Transaction transaction(*this);
 
-    transaction->Begin();
-    uint64_t seq = db_->IncrementGlobalSequence(sequence);
-    transaction->Commit();
+    uint64_t seq = IncrementGlobalSequenceInternal(sequence);
+    transaction.Commit(0);
 
     return seq;
   }
@@ -1318,32 +1608,30 @@
                               const std::string& publicId)
   {
     boost::mutex::scoped_lock lock(mutex_);
-    std::auto_ptr<SQLite::Transaction> transaction(db_->StartTransaction());
-    transaction->Begin();
+    Transaction transaction(*this);
 
     int64_t id;
     ResourceType type;
-    if (!db_->LookupResource(publicId, id, type))
+    if (!db_.LookupResource(id, type, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    db_->LogChange(changeType, id, type);
-
-    transaction->Commit();
+    LogChange(id, changeType, type, publicId);
+    transaction.Commit(0);
   }
 
 
   void ServerIndex::DeleteChanges()
   {
     boost::mutex::scoped_lock lock(mutex_);
-    db_->ClearTable("Changes");
+    db_.ClearChanges();
   }
 
   void ServerIndex::DeleteExportedResources()
   {
     boost::mutex::scoped_lock lock(mutex_);
-    db_->ClearTable("ExportedResources");
+    db_.ClearExportedResources();
   }
 
 
@@ -1370,16 +1658,16 @@
       int64_t resource = toExplore.top();
       toExplore.pop();
 
-      ResourceType thisType = db_->GetResourceType(resource);
+      ResourceType thisType = db_.GetResourceType(resource);
 
       std::list<FileContentType> f;
-      db_->ListAvailableAttachments(f, resource);
+      db_.ListAvailableAttachments(f, resource);
 
       for (std::list<FileContentType>::const_iterator
              it = f.begin(); it != f.end(); ++it)
       {
         FileInfo attachment;
-        if (db_->LookupAttachment(attachment, resource, *it))
+        if (db_.LookupAttachment(attachment, resource, *it))
         {
           compressedSize += attachment.GetCompressedSize();
           uncompressedSize += attachment.GetUncompressedSize();
@@ -1408,7 +1696,7 @@
 
         // Tag all the children of this resource as to be explored
         std::list<int64_t> tmp;
-        db_->GetChildrenInternalId(tmp, resource);
+        db_.GetChildrenInternalId(tmp, resource);
         for (std::list<int64_t>::const_iterator 
                it = tmp.begin(); it != tmp.end(); ++it)
         {
@@ -1437,7 +1725,7 @@
 
     ResourceType type;
     int64_t top;
-    if (!db_->LookupResource(publicId, top, type))
+    if (!db_.LookupResource(top, type, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
@@ -1452,9 +1740,9 @@
 
     target = Json::objectValue;
     target["DiskSize"] = boost::lexical_cast<std::string>(compressedSize);
-    target["DiskSizeMB"] = boost::lexical_cast<unsigned int>(compressedSize / MEGA_BYTES);
+    target["DiskSizeMB"] = static_cast<unsigned int>(compressedSize / MEGA_BYTES);
     target["UncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize);
-    target["UncompressedSizeMB"] = boost::lexical_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
+    target["UncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
 
     switch (type)
     {
@@ -1486,7 +1774,7 @@
 
     ResourceType type;
     int64_t top;
-    if (!db_->LookupResource(publicId, top, type))
+    if (!db_.LookupResource(top, type, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
@@ -1523,20 +1811,20 @@
         int64_t id = that->unstableResources_.RemoveOldest(payload);
 
         // Ensure that the resource is still existing before logging the change
-        if (that->db_->IsExistingResource(id))
+        if (that->db_.IsExistingResource(id))
         {
-          switch (payload.type_)
+          switch (payload.GetResourceType())
           {
-            case Orthanc::ResourceType_Patient:
-              that->db_->LogChange(ChangeType_StablePatient, id, ResourceType_Patient);
+            case ResourceType_Patient:
+              that->LogChange(id, ChangeType_StablePatient, ResourceType_Patient, payload.GetPublicId());
               break;
 
-            case Orthanc::ResourceType_Study:
-              that->db_->LogChange(ChangeType_StableStudy, id, ResourceType_Study);
+            case ResourceType_Study:
+              that->LogChange(id, ChangeType_StableStudy, ResourceType_Study, payload.GetPublicId());
               break;
 
-            case Orthanc::ResourceType_Series:
-              that->db_->LogChange(ChangeType_StableSeries, id, ResourceType_Series);
+            case ResourceType_Series:
+              that->LogChange(id, ChangeType_StableSeries, ResourceType_Series, payload.GetPublicId());
               break;
 
             default:
@@ -1553,7 +1841,8 @@
   
 
   void ServerIndex::MarkAsUnstable(int64_t id,
-                                   Orthanc::ResourceType type)
+                                   Orthanc::ResourceType type,
+                                   const std::string& publicId)
   {
     // WARNING: Before calling this method, "mutex_" must be locked.
 
@@ -1561,68 +1850,72 @@
            type == Orthanc::ResourceType_Study ||
            type == Orthanc::ResourceType_Series);
 
-    unstableResources_.AddOrMakeMostRecent(id, type);
+    UnstableResourcePayload payload(type, publicId);
+    unstableResources_.AddOrMakeMostRecent(id, payload);
     //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id;
+
+    LogChange(id, ChangeType_NewChildInstance, type, publicId);
   }
 
 
 
-  void ServerIndex::LookupTagValue(std::list<std::string>& result,
-                                   DicomTag tag,
-                                   const std::string& value,
-                                   ResourceType type)
+  void ServerIndex::LookupIdentifier(std::list<std::string>& result,
+                                     const DicomTag& tag,
+                                     const std::string& value,
+                                     ResourceType type)
   {
     result.clear();
 
     boost::mutex::scoped_lock lock(mutex_);
 
     std::list<int64_t> id;
-    db_->LookupTagValue(id, tag, value);
+    db_.LookupIdentifier(id, tag, value);
 
     for (std::list<int64_t>::const_iterator 
            it = id.begin(); it != id.end(); ++it)
     {
-      if (db_->GetResourceType(*it) == type)
+      if (db_.GetResourceType(*it) == type)
       {
-        result.push_back(db_->GetPublicId(*it));
+        result.push_back(db_.GetPublicId(*it));
       }
     }
   }
 
 
-  void ServerIndex::LookupTagValue(std::list<std::string>& result,
-                                   DicomTag tag,
-                                   const std::string& value)
+  void ServerIndex::LookupIdentifier(std::list<std::string>& result,
+                                     const DicomTag& tag,
+                                     const std::string& value)
   {
     result.clear();
 
     boost::mutex::scoped_lock lock(mutex_);
 
     std::list<int64_t> id;
-    db_->LookupTagValue(id, tag, value);
+    db_.LookupIdentifier(id, tag, value);
 
     for (std::list<int64_t>::const_iterator 
            it = id.begin(); it != id.end(); ++it)
     {
-      result.push_back(db_->GetPublicId(*it));
+      result.push_back(db_.GetPublicId(*it));
     }
   }
 
 
-  void ServerIndex::LookupTagValue(std::list<std::string>& result,
-                                   const std::string& value)
+  void ServerIndex::LookupIdentifier(std::list< std::pair<ResourceType, std::string> >& result,
+                                     const std::string& value)
   {
     result.clear();
 
     boost::mutex::scoped_lock lock(mutex_);
 
     std::list<int64_t> id;
-    db_->LookupTagValue(id, value);
+    db_.LookupIdentifier(id, value);
 
     for (std::list<int64_t>::const_iterator 
            it = id.begin(); it != id.end(); ++it)
     {
-      result.push_back(db_->GetPublicId(*it));
+      result.push_back(std::make_pair(db_.GetResourceType(*it),
+                                      db_.GetPublicId(*it)));
     }
   }
 
@@ -1636,20 +1929,20 @@
 
     ResourceType resourceType;
     int64_t resourceId;
-    if (!db_->LookupResource(publicId, resourceId, resourceType))
+    if (!db_.LookupResource(resourceId, resourceType, publicId))
     {
       return StoreStatus_Failure;  // Inexistent resource
     }
 
     // Remove possible previous attachment
-    db_->DeleteAttachment(resourceId, attachment.GetContentType());
+    db_.DeleteAttachment(resourceId, attachment.GetContentType());
 
     // Locate the patient of the target resource
     int64_t patientId = resourceId;
     for (;;)
     {
       int64_t parent;
-      if (db_->LookupParent(parent, patientId))
+      if (db_.LookupParent(parent, patientId))
       {
         // We have not reached the patient level yet
         patientId = parent;
@@ -1662,10 +1955,10 @@
     }
 
     // Possibly apply the recycling mechanism while preserving this patient
-    assert(db_->GetResourceType(patientId) == ResourceType_Patient);
-    Recycle(attachment.GetCompressedSize(), db_->GetPublicId(patientId));
+    assert(db_.GetResourceType(patientId) == ResourceType_Patient);
+    Recycle(attachment.GetCompressedSize(), db_.GetPublicId(patientId));
 
-    db_->AddAttachment(resourceId, attachment);
+    db_.AddAttachment(resourceId, attachment);
 
     t.Commit(attachment.GetCompressedSize());
 
@@ -1677,21 +1970,101 @@
                                      FileContentType type)
   {
     boost::mutex::scoped_lock lock(mutex_);
-    listener_->Reset();
-
     Transaction t(*this);
 
     ResourceType rtype;
     int64_t id;
-    if (!db_->LookupResource(publicId, id, rtype))
+    if (!db_.LookupResource(id, rtype, publicId))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
 
-    db_->DeleteAttachment(id, type);
+    db_.DeleteAttachment(id, type);
 
     t.Commit(0);
   }
 
 
+  bool ServerIndex::GetMetadata(Json::Value& target,
+                                const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    target = Json::objectValue;
+
+    ResourceType type;
+    int64_t id;
+    if (!db_.LookupResource(id, type, publicId))
+    {
+      return false;
+    }
+
+    std::list<MetadataType> metadata;
+    db_.ListAvailableMetadata(metadata, id);
+
+    for (std::list<MetadataType>::const_iterator
+           it = metadata.begin(); it != metadata.end(); ++it)
+    {
+      std::string key = EnumerationToString(*it);
+
+      std::string value;
+      if (!db_.LookupMetadata(value, id, *it))
+      {
+        value.clear();
+      }
+
+      target[key] = value;
+    }
+
+    return true;
+  }
+
+
+  void ServerIndex::SetGlobalProperty(GlobalProperty property,
+                                      const std::string& value)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    db_.SetGlobalProperty(property, value);
+  }
+
+
+  std::string ServerIndex::GetGlobalProperty(GlobalProperty property,
+                                             const std::string& defaultValue)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    std::string value;
+    if (db_.LookupGlobalProperty(value, property))
+    {
+      return value;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  bool ServerIndex::GetMainDicomTags(DicomMap& result,
+                                     const std::string& publicId,
+                                     ResourceType expectedType)
+  {
+    result.Clear();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Lookup for the requested resource
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, publicId) ||
+        type != expectedType)
+    {
+      return false;
+    }
+    else
+    {
+      db_.GetMainDicomTags(result, id);
+      return true;
+    }    
+  }
 }
--- a/OrthancServer/ServerIndex.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ServerIndex.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -40,7 +40,7 @@
 #include "../Core/DicomFormat/DicomInstanceHasher.h"
 #include "ServerEnumerations.h"
 
-#include "DatabaseWrapper.h"
+#include "IDatabaseWrapper.h"
 
 
 namespace Orthanc
@@ -54,9 +54,13 @@
 
   class ServerIndex : public boost::noncopyable
   {
+  public:
+    typedef std::list<FileInfo> Attachments;
+    typedef std::map< std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
+
   private:
     class Transaction;
-    struct UnstableResourcePayload;
+    class UnstableResourcePayload;
 
     bool done_;
     boost::mutex mutex_;
@@ -64,7 +68,7 @@
     boost::thread unstableResourcesMonitorThread_;
 
     std::auto_ptr<Internals::ServerIndexListener> listener_;
-    std::auto_ptr<DatabaseWrapper> db_;
+    IDatabaseWrapper& db_;
     LeastRecentlyUsedIndex<int64_t, UnstableResourcePayload>  unstableResources_;
 
     uint64_t currentStorageSize_;
@@ -88,7 +92,8 @@
     void StandaloneRecycling();
 
     void MarkAsUnstable(int64_t id,
-                        Orthanc::ResourceType type);
+                        Orthanc::ResourceType type,
+                        const std::string& publicId);
 
     void GetStatisticsInternal(/* out */ uint64_t& compressedSize, 
                                /* out */ uint64_t& uncompressedSize, 
@@ -98,11 +103,26 @@
                                /* in  */ int64_t id,
                                /* in  */ ResourceType type);
 
-  public:
-    typedef std::list<FileInfo> Attachments;
+    bool GetMetadataAsInteger(int64_t& result,
+                              int64_t id,
+                              MetadataType type);
+
+    void LogChange(int64_t internalId,
+                   ChangeType changeType,
+                   ResourceType resourceType,
+                   const std::string& publicId);
 
+    uint64_t IncrementGlobalSequenceInternal(GlobalProperty property);
+
+    void SetMainDicomTags(int64_t resource,
+                          const DicomMap& tags);
+
+    int64_t CreateResource(const std::string& publicId,
+                           ResourceType type);
+
+  public:
     ServerIndex(ServerContext& context,
-                const std::string& dbPath);
+                IDatabaseWrapper& database);
 
     ~ServerIndex();
 
@@ -122,9 +142,11 @@
     // "count == 0" means no limit on the number of patients
     void SetMaximumPatientCount(unsigned int count);
 
-    StoreStatus Store(const DicomMap& dicomSummary,
+    StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata,
+                      const DicomMap& dicomSummary,
                       const Attachments& attachments,
-                      const std::string& remoteAet);
+                      const std::string& remoteAet,
+                      const MetadataMap& metadata);
 
     void ComputeStatistics(Json::Value& target);                        
 
@@ -136,27 +158,27 @@
                           const std::string& instanceUuid,
                           FileContentType contentType);
 
-    void GetAllUuids(Json::Value& target,
+    void GetAllUuids(std::list<std::string>& target,
                      ResourceType resourceType);
 
-    bool DeleteResource(Json::Value& target,
+    bool DeleteResource(Json::Value& target /* out */,
                         const std::string& uuid,
                         ResourceType expectedType);
 
-    bool GetChanges(Json::Value& target,
+    void GetChanges(Json::Value& target,
                     int64_t since,
                     unsigned int maxResults);
 
-    bool GetLastChange(Json::Value& target);
+    void GetLastChange(Json::Value& target);
 
     void LogExportedResource(const std::string& publicId,
                              const std::string& remoteModality);
 
-    bool GetExportedResources(Json::Value& target,
+    void GetExportedResources(Json::Value& target,
                               int64_t since,
                               unsigned int maxResults);
 
-    bool GetLastExportedResource(Json::Value& target);
+    void GetLastExportedResource(Json::Value& target);
 
     bool IsProtectedPatient(const std::string& publicId);
 
@@ -183,6 +205,9 @@
     void ListAvailableMetadata(std::list<MetadataType>& target,
                                const std::string& publicId);
 
+    bool GetMetadata(Json::Value& target,
+                     const std::string& publicId);
+
     void ListAvailableAttachments(std::list<FileContentType>& target,
                                   const std::string& publicId,
                                   ResourceType expectedType);
@@ -209,22 +234,32 @@
                        /* out */ unsigned int& countInstances, 
                        const std::string& publicId);
 
-    void LookupTagValue(std::list<std::string>& result,
-                        DicomTag tag,
-                        const std::string& value,
-                        ResourceType type);
+    void LookupIdentifier(std::list<std::string>& result,
+                          const DicomTag& tag,
+                          const std::string& value,
+                          ResourceType type);
 
-    void LookupTagValue(std::list<std::string>& result,
-                        DicomTag tag,
-                        const std::string& value);
+    void LookupIdentifier(std::list<std::string>& result,
+                          const DicomTag& tag,
+                          const std::string& value);
 
-    void LookupTagValue(std::list<std::string>& result,
-                        const std::string& value);
+    void LookupIdentifier(std::list< std::pair<ResourceType, std::string> >& result,
+                          const std::string& value);
 
     StoreStatus AddAttachment(const FileInfo& attachment,
                               const std::string& publicId);
 
     void DeleteAttachment(const std::string& publicId,
                           FileContentType type);
+
+    void SetGlobalProperty(GlobalProperty property,
+                           const std::string& value);
+
+    std::string GetGlobalProperty(GlobalProperty property,
+                                  const std::string& defaultValue);
+
+    bool GetMainDicomTags(DicomMap& result,
+                          const std::string& publicId,
+                          ResourceType expectedType);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerIndexChange.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,113 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ServerEnumerations.h"
+#include "../Core/Toolbox.h"
+
+#include <string>
+#include <json/value.h>
+
+namespace Orthanc
+{
+  struct ServerIndexChange
+  {
+  private:
+    int64_t      seq_;
+    ChangeType   changeType_;
+    ResourceType resourceType_;
+    std::string  publicId_;
+    std::string  date_;
+
+  public:
+    ServerIndexChange(ChangeType changeType,
+                      ResourceType resourceType,
+                      const std::string& publicId) :
+      seq_(-1),
+      changeType_(changeType),
+      resourceType_(resourceType),
+      publicId_(publicId),
+      date_(Toolbox::GetNowIsoString())
+    {
+    }
+
+    ServerIndexChange(int64_t seq,
+                      ChangeType changeType,
+                      ResourceType resourceType,
+                      const std::string& publicId,
+                      const std::string& date) :
+      seq_(seq),
+      changeType_(changeType),
+      resourceType_(resourceType),
+      publicId_(publicId),
+      date_(date)
+    {
+    }
+
+    int64_t  GetSeq() const
+    {
+      return seq_;
+    }
+
+    ChangeType  GetChangeType() const
+    {
+      return changeType_;
+    }
+
+    ResourceType  GetResourceType() const
+    {
+      return resourceType_;
+    }
+
+    const std::string&  GetPublicId() const
+    {
+      return publicId_;
+    }
+
+    const std::string& GetDate() const
+    {
+      return date_;
+    }
+
+    void Format(Json::Value& item) const
+    {
+      item = Json::objectValue;
+      item["Seq"] = static_cast<int>(seq_);
+      item["ChangeType"] = EnumerationToString(changeType_);
+      item["ResourceType"] = EnumerationToString(resourceType_);
+      item["ID"] = publicId_;
+      item["Path"] = GetBasePath(resourceType_, publicId_);
+      item["Date"] = date_;
+    }
+  };
+}
--- a/OrthancServer/ServerToolbox.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ServerToolbox.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/ServerToolbox.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ServerToolbox.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/ToDcmtkBridge.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ToDcmtkBridge.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/OrthancServer/ToDcmtkBridge.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/ToDcmtkBridge.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Upgrade4To5.sql	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,66 @@
+-- This SQLite script updates the version of the Orthanc database from 4 to 5.
+
+
+-- Remove 2 indexes to speed up
+
+DROP INDEX MainDicomTagsIndex2;
+DROP INDEX MainDicomTagsIndexValues;
+
+
+-- Add a new table to index the DICOM identifiers
+
+CREATE TABLE DicomIdentifiers(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       tagGroup INTEGER,
+       tagElement INTEGER,
+       value TEXT,
+       PRIMARY KEY(id, tagGroup, tagElement)
+       );
+
+CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
+CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
+CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY);
+
+
+-- Migrate data from MainDicomTags to MainResourcesTags and MainInstancesTags
+
+INSERT INTO DicomIdentifiers SELECT * FROM MainDicomTags
+       WHERE ((tagGroup = 16 AND tagElement = 32) OR  -- PatientID (0x0010, 0x0020)
+              (tagGroup = 32 AND tagElement = 13) OR  -- StudyInstanceUID (0x0020, 0x000d)
+              (tagGroup = 8  AND tagElement = 80) OR  -- AccessionNumber (0x0008, 0x0050)
+              (tagGroup = 32 AND tagElement = 14) OR  -- SeriesInstanceUID (0x0020, 0x000e)
+              (tagGroup = 8  AND tagElement = 24));   -- SOPInstanceUID (0x0008, 0x0018)
+
+DELETE FROM MainDicomTags
+       WHERE ((tagGroup = 16 AND tagElement = 32) OR  -- PatientID (0x0010, 0x0020)
+              (tagGroup = 32 AND tagElement = 13) OR  -- StudyInstanceUID (0x0020, 0x000d)
+              (tagGroup = 8  AND tagElement = 80) OR  -- AccessionNumber (0x0008, 0x0050)
+              (tagGroup = 32 AND tagElement = 14) OR  -- SeriesInstanceUID (0x0020, 0x000e)
+              (tagGroup = 8  AND tagElement = 24));   -- SOPInstanceUID (0x0008, 0x0018)
+
+
+-- Upgrade the "ResourceDeleted" trigger
+
+DROP TRIGGER ResourceDeleted;
+DROP TRIGGER ResourceDeletedParentCleaning;
+
+CREATE TRIGGER ResourceDeleted
+AFTER DELETE ON Resources
+BEGIN
+  SELECT SignalResourceDeleted(old.publicId, old.resourceType);
+  SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
+    FROM Resources AS parent WHERE internalId = old.parentId;
+END;
+
+CREATE TRIGGER ResourceDeletedParentCleaning
+AFTER DELETE ON Resources
+FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0
+BEGIN
+  DELETE FROM Resources WHERE internalId = old.parentId;
+END;
+
+
+-- Change the database version
+-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
+
+UPDATE GlobalProperties SET value="5" WHERE property=1;
--- a/OrthancServer/main.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/OrthancServer/main.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -37,6 +37,7 @@
 #include <glog/logging.h>
 #include <boost/algorithm/string/predicate.hpp>
 
+#include "../Core/Uuid.h"
 #include "../Core/HttpServer/EmbeddedResourceHttpHandler.h"
 #include "../Core/HttpServer/FilesystemHttpHandler.h"
 #include "../Core/Lua/LuaFunctionCall.h"
@@ -48,10 +49,14 @@
 #include "OrthancFindRequestHandler.h"
 #include "OrthancMoveRequestHandler.h"
 #include "ServerToolbox.h"
+#include "../Plugins/Engine/PluginsManager.h"
+#include "../Plugins/Engine/OrthancPlugins.h"
 
 using namespace Orthanc;
 
 
+#define ENABLE_PLUGINS  1
+
 
 class OrthancStoreRequestHandler : public IStoreRequestHandler
 {
@@ -67,11 +72,20 @@
   virtual void Handle(const std::string& dicomFile,
                       const DicomMap& dicomSummary,
                       const Json::Value& dicomJson,
-                      const std::string& remoteAet)
+                      const std::string& remoteAet,
+                      const std::string& calledAet)
   {
     if (dicomFile.size() > 0)
     {
-      server_.Store(&dicomFile[0], dicomFile.size(), dicomSummary, dicomJson, remoteAet);
+      DicomInstanceToStore toStore;
+      toStore.SetBuffer(dicomFile);
+      toStore.SetSummary(dicomSummary);
+      toStore.SetJson(dicomJson);
+      toStore.SetRemoteAet(remoteAet);
+      toStore.SetCalledAet(calledAet);
+
+      std::string id;
+      server_.Store(id, toStore);
     }
   }
 };
@@ -139,7 +153,14 @@
 
 class OrthancApplicationEntityFilter : public IApplicationEntityFilter
 {
+private:
+  ServerContext& context_;
+
 public:
+  OrthancApplicationEntityFilter(ServerContext& context) : context_(context)
+  {
+  }
+
   virtual bool IsAllowedConnection(const std::string& /*callingIp*/,
                                    const std::string& /*callingAet*/)
   {
@@ -166,6 +187,63 @@
       return true;
     }
   }
+
+  virtual bool IsAllowedTransferSyntax(const std::string& callingIp,
+                                       const std::string& callingAet,
+                                       TransferSyntax syntax)
+  {
+    std::string configuration;
+
+    switch (syntax)
+    {
+      case TransferSyntax_Deflated:
+        configuration = "DeflatedTransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Jpeg:
+        configuration = "JpegTransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Jpeg2000:
+        configuration = "Jpeg2000TransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_JpegLossless:
+        configuration = "JpegLosslessTransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Jpip:
+        configuration = "JpipTransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Mpeg2:
+        configuration = "Mpeg2TransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Rle:
+        configuration = "RleTransferSyntaxAccepted";
+        break;
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    {
+      std::string lua = "Is" + configuration;
+
+      ServerContext::LuaContextLocker locker(context_);
+      
+      if (locker.GetLua().IsExistingFunction(lua.c_str()))
+      {
+        LuaFunctionCall call(locker.GetLua(), lua.c_str());
+        call.PushString(callingAet);
+        call.PushString(callingIp);
+        return call.ExecutePredicate();
+      }
+    }
+
+    return Configuration::GetGlobalBoolParameter(configuration, true);
+  }
 };
 
 
@@ -186,10 +264,12 @@
   {
     static const char* HTTP_FILTER = "IncomingHttpRequestFilter";
 
+    ServerContext::LuaContextLocker locker(context_);
+
     // Test if the instance must be filtered out
-    if (context_.GetLuaContext().IsExistingFunction(HTTP_FILTER))
+    if (locker.GetLua().IsExistingFunction(HTTP_FILTER))
     {
-      LuaFunctionCall call(context_.GetLuaContext(), HTTP_FILTER);
+      LuaFunctionCall call(locker.GetLua(), HTTP_FILTER);
 
       switch (method)
       {
@@ -229,7 +309,7 @@
 };
 
 
-void PrintHelp(char* path)
+static void PrintHelp(char* path)
 {
   std::cout 
     << "Usage: " << path << " [OPTION]... [CONFIGURATION]" << std::endl
@@ -256,11 +336,11 @@
 }
 
 
-void PrintVersion(char* path)
+static void PrintVersion(char* path)
 {
   std::cout
     << path << " " << ORTHANC_VERSION << std::endl
-    << "Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege (Belgium) " << std::endl
+    << "Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl
     << "Licensing GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>, with OpenSSL exception." << std::endl
     << "This is free software: you are free to change and redistribute it." << std::endl
     << "There is NO WARRANTY, to the extent permitted by law." << std::endl
@@ -269,6 +349,221 @@
 }
 
 
+
+static void LoadLuaScripts(ServerContext& context)
+{
+  std::list<std::string> luaScripts;
+  Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts");
+  for (std::list<std::string>::const_iterator
+         it = luaScripts.begin(); it != luaScripts.end(); ++it)
+  {
+    std::string path = Configuration::InterpretStringParameterAsPath(*it);
+    LOG(WARNING) << "Installing the Lua scripts from: " << path;
+    std::string script;
+    Toolbox::ReadFile(script, path);
+
+    ServerContext::LuaContextLocker locker(context);
+    locker.GetLua().Execute(script);
+  }
+}
+
+
+static void LoadPlugins(PluginsManager& pluginsManager)
+{
+  std::list<std::string> plugins;
+  Configuration::GetGlobalListOfStringsParameter(plugins, "Plugins");
+  for (std::list<std::string>::const_iterator
+         it = plugins.begin(); it != plugins.end(); ++it)
+  {
+    std::string path = Configuration::InterpretStringParameterAsPath(*it);
+    LOG(WARNING) << "Loading plugin(s) from: " << path;
+    pluginsManager.RegisterPlugin(path);
+  }  
+}
+
+
+
+static bool StartOrthanc(int argc, char *argv[])
+{
+#if ENABLE_PLUGINS == 1
+  OrthancPlugins orthancPlugins;
+  orthancPlugins.SetCommandLineArguments(argc, argv);
+  PluginsManager pluginsManager;
+  pluginsManager.RegisterServiceProvider(orthancPlugins);
+  LoadPlugins(pluginsManager);
+#endif
+
+  // "storage" and "database" must be declared BEFORE "ServerContext
+  // context", to avoid mess in the invokation order of the destructors.
+  std::auto_ptr<IDatabaseWrapper> database;
+  std::auto_ptr<IStorageArea>  storage;
+  std::auto_ptr<ServerContext> context;
+
+  if (orthancPlugins.HasDatabase())
+  {
+    context.reset(new ServerContext(orthancPlugins.GetDatabase()));
+  }
+  else
+  {
+    database.reset(Configuration::CreateDatabaseWrapper());
+    context.reset(new ServerContext(*database));
+  }
+
+  context->SetCompressionEnabled(Configuration::GetGlobalBoolParameter("StorageCompression", false));
+  context->SetStoreMD5ForAttachments(Configuration::GetGlobalBoolParameter("StoreMD5ForAttachments", true));
+
+  LoadLuaScripts(*context);
+
+  try
+  {
+    context->GetIndex().SetMaximumPatientCount(Configuration::GetGlobalIntegerParameter("MaximumPatientCount", 0));
+  }
+  catch (...)
+  {
+    context->GetIndex().SetMaximumPatientCount(0);
+  }
+
+  try
+  {
+    uint64_t size = Configuration::GetGlobalIntegerParameter("MaximumStorageSize", 0);
+    context->GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
+  }
+  catch (...)
+  {
+    context->GetIndex().SetMaximumStorageSize(0);
+  }
+
+  MyDicomServerFactory serverFactory(*context);
+  bool isReset = false;
+    
+  {
+    // DICOM server
+    DicomServer dicomServer;
+    OrthancApplicationEntityFilter dicomFilter(*context);
+    dicomServer.SetCalledApplicationEntityTitleCheck(Configuration::GetGlobalBoolParameter("DicomCheckCalledAet", false));
+    dicomServer.SetStoreRequestHandlerFactory(serverFactory);
+    dicomServer.SetMoveRequestHandlerFactory(serverFactory);
+    dicomServer.SetFindRequestHandlerFactory(serverFactory);
+    dicomServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("DicomPort", 4242));
+    dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"));
+    dicomServer.SetApplicationEntityFilter(dicomFilter);
+
+    // HTTP server
+    MyIncomingHttpRequestFilter httpFilter(*context);
+    MongooseServer httpServer;
+    httpServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("HttpPort", 8042));
+    httpServer.SetRemoteAccessAllowed(Configuration::GetGlobalBoolParameter("RemoteAccessAllowed", false));
+    httpServer.SetKeepAliveEnabled(Configuration::GetGlobalBoolParameter("KeepAlive", false));
+    httpServer.SetIncomingHttpRequestFilter(httpFilter);
+
+    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);
+    }
+
+    OrthancRestApi restApi(*context);
+
+#if ORTHANC_STANDALONE == 1
+    EmbeddedResourceHttpHandler staticResources("/app", EmbeddedResources::ORTHANC_EXPLORER);
+#else
+    FilesystemHttpHandler staticResources("/app", ORTHANC_PATH "/OrthancExplorer");
+#endif
+
+#if ENABLE_PLUGINS == 1
+    orthancPlugins.SetServerContext(*context);
+    httpServer.RegisterHandler(orthancPlugins);
+    context->SetOrthancPlugins(pluginsManager, orthancPlugins);
+#endif
+
+    httpServer.RegisterHandler(staticResources);
+    httpServer.RegisterHandler(restApi);
+
+
+#if ENABLE_PLUGINS == 1
+    // Prepare the storage area
+    if (orthancPlugins.HasStorageArea())
+    {
+      LOG(WARNING) << "Using a custom storage area from plugins";
+      storage.reset(orthancPlugins.GetStorageArea());
+    }
+    else
+#endif
+    {
+      storage.reset(Configuration::CreateStorageArea());
+    }
+    
+    context->SetStorageArea(*storage);
+
+
+    // GO !!! Start the requested servers
+    if (Configuration::GetGlobalBoolParameter("HttpServerEnabled", true))
+    {
+#if ENABLE_PLUGINS == 1
+      orthancPlugins.SetOrthancRestApi(restApi);
+#endif
+
+      httpServer.Start();
+      LOG(WARNING) << "HTTP server listening on port: " << httpServer.GetPortNumber();
+    }
+    else
+    {
+      LOG(WARNING) << "The HTTP server is disabled";
+    }
+
+    if (Configuration::GetGlobalBoolParameter("DicomServerEnabled", true))
+    {
+      dicomServer.Start();
+      LOG(WARNING) << "DICOM server listening on port: " << dicomServer.GetPortNumber();
+    }
+    else
+    {
+      LOG(WARNING) << "The DICOM server is disabled";
+    }
+
+    LOG(WARNING) << "Orthanc has started";
+    Toolbox::ServerBarrier(restApi.ResetRequestReceivedFlag());
+    isReset = restApi.ResetRequestReceivedFlag();
+
+    if (isReset)
+    {
+      LOG(WARNING) << "Reset request received, restarting Orthanc";
+    }
+
+    // We're done
+    LOG(WARNING) << "Orthanc is stopping";
+
+#if ENABLE_PLUGINS == 1
+    context->ResetOrthancPlugins();
+    orthancPlugins.Stop();
+    orthancPlugins.ResetOrthancRestApi();
+    LOG(WARNING) << "    Plugins have stopped";
+#endif
+
+    dicomServer.Stop();
+    LOG(WARNING) << "    DICOM server has stopped";
+
+    httpServer.Stop();
+    LOG(WARNING) << "    HTTP server has stopped";
+  }
+
+  serverFactory.Done();
+
+  return isReset;
+}
+
+
+
+
 int main(int argc, char* argv[]) 
 {
   // Initialize Google's logging library.
@@ -327,178 +622,78 @@
 
   google::InitGoogleLogging("Orthanc");
 
+  const char* configurationFile = NULL;
+  for (int i = 1; i < argc; i++)
+  {
+    // Use the first argument that does not start with a "-" as
+    // the configuration file
+    if (argv[i][0] != '-')
+    {
+      configurationFile = argv[i];
+    }
+  }
+
+  LOG(WARNING) << "Orthanc version: " << ORTHANC_VERSION;
+
   int status = 0;
   try
   {
-    bool isInitialized = false;
-    if (argc >= 2)
+    if (1)
     {
-      for (int i = 1; i < argc; i++)
-      {
-        // Use the first argument that does not start with a "-" as
-        // the configuration file
-        if (argv[i][0] != '-')
-        {
-          OrthancInitialize(argv[i]);
-          isInitialized = true;
-        }
-      }
-    }
-
-    if (!isInitialized)
-    {
-      OrthancInitialize();
-    }
+      DicomUserConnection c;
+      c.SetLocalApplicationEntityTitle("ORTHANC");
+      c.SetRemoteApplicationEntityTitle("ORTHANC");
+      c.SetRemoteHost("localhost");
+      c.SetRemotePort(4343);
+      c.Open();
 
-    std::string storageDirectoryStr = Configuration::GetGlobalStringParameter("StorageDirectory", "OrthancStorage");
-    boost::filesystem::path storageDirectory = Configuration::InterpretStringParameterAsPath(storageDirectoryStr);
-    boost::filesystem::path indexDirectory = Configuration::InterpretStringParameterAsPath(
-        Configuration::GetGlobalStringParameter("IndexDirectory", storageDirectoryStr));
-    ServerContext context(storageDirectory, indexDirectory);
-
-    LOG(WARNING) << "Storage directory: " << storageDirectory;
-    LOG(WARNING) << "Index directory: " << indexDirectory;
+      DicomMap m; // Delphine
+      m.SetValue(DICOM_TAG_PATIENT_ID, "5423962");
+      m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.840.113845.11.1000000001951524609.20121203131451.1457891");
+      m.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "1.2.840.113619.2.278.3.262930758.589.1354512768.115");
+      m.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "1.3.12.2.1107.5.2.33.37097.2012041613043195815872177");
 
-    context.SetCompressionEnabled(Configuration::GetGlobalBoolParameter("StorageCompression", false));
-    context.SetStoreMD5ForAttachments(Configuration::GetGlobalBoolParameter("StoreMD5ForAttachments", true));
+      DicomFindAnswers fnd;
+      c.FindInstance(fnd, m);
+      //c.FindSeries(fnd, m);
 
-    std::list<std::string> luaScripts;
-    Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts");
-    for (std::list<std::string>::const_iterator
-           it = luaScripts.begin(); it != luaScripts.end(); ++it)
-    {
-      std::string path = Configuration::InterpretStringParameterAsPath(*it);
-      LOG(WARNING) << "Installing the Lua scripts from: " << path;
-      std::string script;
-      Toolbox::ReadFile(script, path);
-      context.GetLuaContext().Execute(script);
+      printf("ok %d\n", fnd.GetSize());
     }
 
 
-    try
-    {
-      context.GetIndex().SetMaximumPatientCount(Configuration::GetGlobalIntegerParameter("MaximumPatientCount", 0));
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumPatientCount(0);
-    }
-
-    try
-    {
-      uint64_t size = Configuration::GetGlobalIntegerParameter("MaximumStorageSize", 0);
-      context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumStorageSize(0);
-    }
-
-    MyDicomServerFactory serverFactory(context);
-    
+    for (;;)
     {
-      // DICOM server
-      DicomServer dicomServer;
-      OrthancApplicationEntityFilter dicomFilter;
-      dicomServer.SetCalledApplicationEntityTitleCheck(Configuration::GetGlobalBoolParameter("DicomCheckCalledAet", false));
-      dicomServer.SetStoreRequestHandlerFactory(serverFactory);
-      dicomServer.SetMoveRequestHandlerFactory(serverFactory);
-      dicomServer.SetFindRequestHandlerFactory(serverFactory);
-      dicomServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("DicomPort", 4242));
-      dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"));
-      dicomServer.SetApplicationEntityFilter(dicomFilter);
+      OrthancInitialize(configurationFile);
 
-      // HTTP server
-      MyIncomingHttpRequestFilter httpFilter(context);
-      MongooseServer httpServer;
-      httpServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("HttpPort", 8042));
-      httpServer.SetRemoteAccessAllowed(Configuration::GetGlobalBoolParameter("RemoteAccessAllowed", false));
-      httpServer.SetIncomingHttpRequestFilter(httpFilter);
-
-      httpServer.SetAuthenticationEnabled(Configuration::GetGlobalBoolParameter("AuthenticationEnabled", false));
-      Configuration::SetupRegisteredUsers(httpServer);
-
-      if (Configuration::GetGlobalBoolParameter("SslEnabled", false))
+      bool reset = StartOrthanc(argc, argv);
+      if (reset)
       {
-        std::string certificate = Configuration::InterpretStringParameterAsPath(
-          Configuration::GetGlobalStringParameter("SslCertificate", "certificate.pem"));
-        httpServer.SetSslEnabled(true);
-        httpServer.SetSslCertificate(certificate.c_str());
+        OrthancFinalize();
       }
       else
       {
-        httpServer.SetSslEnabled(false);
-      }
-
-#if ORTHANC_STANDALONE == 1
-      httpServer.RegisterHandler(new EmbeddedResourceHttpHandler("/app", EmbeddedResources::ORTHANC_EXPLORER));
-#else
-      httpServer.RegisterHandler(new FilesystemHttpHandler("/app", ORTHANC_PATH "/OrthancExplorer"));
-#endif
-
-      httpServer.RegisterHandler(new OrthancRestApi(context));
-
-      // GO !!! Start the requested servers
-      if (Configuration::GetGlobalBoolParameter("HttpServerEnabled", true))
-      {
-        httpServer.Start();
-        LOG(WARNING) << "HTTP server listening on port: " << httpServer.GetPortNumber();
-      }
-      else
-      {
-        LOG(WARNING) << "The HTTP server is disabled";
-      }
-
-      if (Configuration::GetGlobalBoolParameter("DicomServerEnabled", true))
-      {
-        dicomServer.Start();
-        LOG(WARNING) << "DICOM server listening on port: " << dicomServer.GetPortNumber();
-      }
-      else
-      {
-        LOG(WARNING) << "The DICOM server is disabled";
+        break;
       }
-
-
-      {
-        DicomUserConnection c;
-        c.SetLocalApplicationEntityTitle("ORTHANC");
-        c.SetDistantApplicationEntityTitle("ORTHANC");
-        c.SetDistantHost("localhost");
-        c.SetDistantPort(4343);
-        c.Open();
-
-        DicomMap m; // Delphine
-        m.SetValue(DICOM_TAG_PATIENT_ID, "5423962");
-        m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "1.2.840.113845.11.1000000001951524609.20121203131451.1457891");
-        m.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "1.2.840.113619.2.278.3.262930758.589.1354512768.115");
-        m.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "1.3.12.2.1107.5.2.33.37097.2012041613043195815872177");
-
-        DicomFindAnswers fnd;
-        c.FindInstance(fnd, m);
-        //c.FindSeries(fnd, m);
-
-        printf("ok %d\n", fnd.GetSize());
-      }
-
-
-      LOG(WARNING) << "Orthanc has started";
-      Toolbox::ServerBarrier();
-
-      // We're done
-      LOG(WARNING) << "Orthanc is stopping";
     }
-
-    serverFactory.Done();
+  }
+  catch (const OrthancException& e)
+  {
+    LOG(ERROR) << "Uncaught exception, stopping now: [" << e.What() << "]";
+    status = -1;
   }
-  catch (OrthancException& e)
+  catch (const std::exception& e) 
   {
-    LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
+    LOG(ERROR) << "Uncaught exception, stopping now: [" << e.what() << "]";
+    status = -1;
+  }
+  catch (const std::string& s) 
+  {
+    LOG(ERROR) << "Uncaught exception, stopping now: [" << s << "]";
     status = -1;
   }
   catch (...)
   {
-    LOG(ERROR) << "NATIVE EXCEPTION";
+    LOG(ERROR) << "Native exception, stopping now. Check your plugins, if any.";
     status = -1;
   }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/IPluginServiceProvider.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,51 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "../Include/OrthancCPlugin.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IPluginServiceProvider : boost::noncopyable
+  {
+  public:
+    virtual ~IPluginServiceProvider()
+    {
+    }
+
+    virtual bool InvokeService(_OrthancPluginService service,
+                               const void* parameters) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/OrthancPluginDatabase.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,1113 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "OrthancPluginDatabase.h"
+
+#include "../../Core/OrthancException.h"
+
+#include <cassert>
+#include <glog/logging.h>
+
+namespace Orthanc
+{
+  static OrthancPluginResourceType Convert(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return OrthancPluginResourceType_Patient;
+
+      case ResourceType_Study:
+        return OrthancPluginResourceType_Study;
+
+      case ResourceType_Series:
+        return OrthancPluginResourceType_Series;
+
+      case ResourceType_Instance:
+        return OrthancPluginResourceType_Instance;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  static ResourceType Convert(OrthancPluginResourceType type)
+  {
+    switch (type)
+    {
+      case OrthancPluginResourceType_Patient:
+        return ResourceType_Patient;
+
+      case OrthancPluginResourceType_Study:
+        return ResourceType_Study;
+
+      case OrthancPluginResourceType_Series:
+        return ResourceType_Series;
+
+      case OrthancPluginResourceType_Instance:
+        return ResourceType_Instance;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  static FileInfo Convert(const OrthancPluginAttachment& attachment)
+  {
+    return FileInfo(attachment.uuid,
+                    static_cast<FileContentType>(attachment.contentType),
+                    attachment.uncompressedSize,
+                    attachment.uncompressedHash,
+                    static_cast<CompressionType>(attachment.compressionType),
+                    attachment.compressedSize,
+                    attachment.compressedHash);
+  }
+
+
+  void OrthancPluginDatabase::ResetAnswers()
+  {
+    type_ = _OrthancPluginDatabaseAnswerType_None;
+
+    answerDicomMap_ = NULL;
+    answerChanges_ = NULL;
+    answerExportedResources_ = NULL;
+    answerDone_ = NULL;
+  }
+
+
+  void OrthancPluginDatabase::ForwardAnswers(std::list<int64_t>& target)
+  {
+    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+        type_ != _OrthancPluginDatabaseAnswerType_Int64)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    target.clear();
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_Int64)
+    {
+      for (std::list<int64_t>::const_iterator 
+             it = answerInt64_.begin(); it != answerInt64_.end(); ++it)
+      {
+        target.push_back(*it);
+      }
+    }
+  }
+
+
+  void OrthancPluginDatabase::ForwardAnswers(std::list<std::string>& target)
+  {
+    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+        type_ != _OrthancPluginDatabaseAnswerType_String)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    target.clear();
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_String)
+    {
+      for (std::list<std::string>::const_iterator 
+             it = answerStrings_.begin(); it != answerStrings_.end(); ++it)
+      {
+        target.push_back(*it);
+      }
+    }
+  }
+
+
+  bool OrthancPluginDatabase::ForwardSingleAnswer(std::string& target)
+  {
+    if (type_ == _OrthancPluginDatabaseAnswerType_None)
+    {
+      return false;
+    }
+    else if (type_ == _OrthancPluginDatabaseAnswerType_String &&
+             answerStrings_.size() == 1)
+    {
+      target = answerStrings_.front();
+      return true; 
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  bool OrthancPluginDatabase::ForwardSingleAnswer(int64_t& target)
+  {
+    if (type_ == _OrthancPluginDatabaseAnswerType_None)
+    {
+      return false;
+    }
+    else if (type_ == _OrthancPluginDatabaseAnswerType_Int64 &&
+             answerInt64_.size() == 1)
+    {
+      target = answerInt64_.front();
+      return true; 
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  OrthancPluginDatabase::OrthancPluginDatabase(const OrthancPluginDatabaseBackend& backend,
+                                               void *payload) : 
+    type_(_OrthancPluginDatabaseAnswerType_None),
+    backend_(backend),
+    payload_(payload),
+    listener_(NULL),
+    answerDicomMap_(NULL),
+    answerChanges_(NULL),
+    answerExportedResources_(NULL),
+    answerDone_(NULL)
+  {
+  }
+
+
+  void OrthancPluginDatabase::AddAttachment(int64_t id,
+                                            const FileInfo& attachment)
+  {
+    OrthancPluginAttachment tmp;
+    tmp.uuid = attachment.GetUuid().c_str();
+    tmp.contentType = static_cast<int32_t>(attachment.GetContentType());
+    tmp.uncompressedSize = attachment.GetUncompressedSize();
+    tmp.uncompressedHash = attachment.GetUncompressedMD5().c_str();
+    tmp.compressionType = static_cast<int32_t>(attachment.GetCompressionType());
+    tmp.compressedSize = attachment.GetCompressedSize();
+    tmp.compressedHash = attachment.GetCompressedMD5().c_str();
+
+    if (backend_.addAttachment(payload_, id, &tmp) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::AttachChild(int64_t parent,
+                                          int64_t child)
+  {
+    if (backend_.attachChild(payload_, parent, child) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::ClearChanges()
+  {
+    if (backend_.clearChanges(payload_) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::ClearExportedResources()
+  {
+    if (backend_.clearExportedResources(payload_) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  int64_t OrthancPluginDatabase::CreateResource(const std::string& publicId,
+                                                ResourceType type)
+  {
+    int64_t id;
+
+    if (backend_.createResource(&id, payload_, publicId.c_str(), Convert(type)) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return id;
+  }
+
+
+  void OrthancPluginDatabase::DeleteAttachment(int64_t id,
+                                               FileContentType attachment)
+  {
+    if (backend_.deleteAttachment(payload_, id, static_cast<int32_t>(attachment)) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::DeleteMetadata(int64_t id,
+                                             MetadataType type)
+  {
+    if (backend_.deleteMetadata(payload_, id, static_cast<int32_t>(type)) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::DeleteResource(int64_t id)
+  {
+    if (backend_.deleteResource(payload_, id) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                             int64_t id)
+  {
+    std::list<MetadataType> metadata;
+    ListAvailableMetadata(metadata, id);
+
+    target.clear();
+
+    for (std::list<MetadataType>::const_iterator
+           it = metadata.begin(); it != metadata.end(); ++it)
+    {
+      std::string value;
+      if (!LookupMetadata(value, id, *it))
+      {
+        throw OrthancException(ErrorCode_Plugin);
+      }
+
+      target[*it] = value;
+    }
+  }
+
+
+  void OrthancPluginDatabase::GetAllPublicIds(std::list<std::string>& target,
+                                              ResourceType resourceType)
+  {
+    ResetAnswers();
+
+    if (backend_.getAllPublicIds(GetContext(), payload_, Convert(resourceType)) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    ForwardAnswers(target);
+  }
+
+
+
+  void OrthancPluginDatabase::GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                                         bool& done /*out*/,
+                                         int64_t since,
+                                         uint32_t maxResults)
+  {
+    ResetAnswers();
+    answerChanges_ = &target;
+    answerDone_ = &done;
+    done = false;
+
+    if (backend_.getChanges(GetContext(), payload_, since, maxResults) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::GetChildrenInternalId(std::list<int64_t>& target,
+                                                    int64_t id)
+  {
+    ResetAnswers();
+
+    if (backend_.getChildrenInternalId(GetContext(), payload_, id) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    ForwardAnswers(target);
+  }
+
+
+  void OrthancPluginDatabase::GetChildrenPublicId(std::list<std::string>& target,
+                                                  int64_t id)
+  {
+    ResetAnswers();
+
+    if (backend_.getChildrenPublicId(GetContext(), payload_, id) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    ForwardAnswers(target);
+  }
+
+
+  void OrthancPluginDatabase::GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                                   bool& done /*out*/,
+                                                   int64_t since,
+                                                   uint32_t maxResults)
+  {
+    ResetAnswers();
+    answerExportedResources_ = &target;
+    answerDone_ = &done;
+    done = false;
+
+    if (backend_.getExportedResources(GetContext(), payload_, since, maxResults) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
+  {
+    bool ignored = false;
+
+    ResetAnswers();
+    answerChanges_ = &target;
+    answerDone_ = &ignored;
+
+    if (backend_.getLastChange(GetContext(), payload_) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
+  {
+    bool ignored = false;
+
+    ResetAnswers();
+    answerExportedResources_ = &target;
+    answerDone_ = &ignored;
+
+    if (backend_.getLastExportedResource(GetContext(), payload_) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::GetMainDicomTags(DicomMap& map,
+                                               int64_t id)
+  {
+    ResetAnswers();
+    answerDicomMap_ = &map;
+
+    if (backend_.getMainDicomTags(GetContext(), payload_, id) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  std::string OrthancPluginDatabase::GetPublicId(int64_t resourceId)
+  {
+    ResetAnswers();
+    std::string s;
+
+    if (backend_.getPublicId(GetContext(), payload_, resourceId) != 0 ||
+        !ForwardSingleAnswer(s))
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return s;
+  }
+
+
+  uint64_t OrthancPluginDatabase::GetResourceCount(ResourceType resourceType)
+  {
+    uint64_t count;
+
+    if (backend_.getResourceCount(&count, payload_, Convert(resourceType)) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return count;
+  }
+
+
+  ResourceType OrthancPluginDatabase::GetResourceType(int64_t resourceId)
+  {
+    OrthancPluginResourceType type;
+
+    if (backend_.getResourceType(&type, payload_, resourceId) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return Convert(type);
+  }
+
+
+  uint64_t OrthancPluginDatabase::GetTotalCompressedSize()
+  {
+    uint64_t size;
+
+    if (backend_.getTotalCompressedSize(&size, payload_) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return size;
+  }
+
+    
+  uint64_t OrthancPluginDatabase::GetTotalUncompressedSize()
+  {
+    uint64_t size;
+
+    if (backend_.getTotalUncompressedSize(&size, payload_) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return size;
+  }
+
+
+  bool OrthancPluginDatabase::IsExistingResource(int64_t internalId)
+  {
+    int32_t existing;
+
+    if (backend_.isExistingResource(&existing, payload_, internalId) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return existing;
+  }
+
+
+  bool OrthancPluginDatabase::IsProtectedPatient(int64_t internalId)
+  {
+    int32_t isProtected;
+
+    if (backend_.isProtectedPatient(&isProtected, payload_, internalId) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return isProtected;
+  }
+
+
+  void OrthancPluginDatabase::ListAvailableMetadata(std::list<MetadataType>& target,
+                                                    int64_t id)
+  {
+    ResetAnswers();
+
+    if (backend_.listAvailableMetadata(GetContext(), payload_, id) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+        type_ != _OrthancPluginDatabaseAnswerType_Int32)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    target.clear();
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
+    {
+      for (std::list<int32_t>::const_iterator 
+             it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
+      {
+        target.push_back(static_cast<MetadataType>(*it));
+      }
+    }
+  }
+
+
+  void OrthancPluginDatabase::ListAvailableAttachments(std::list<FileContentType>& target,
+                                                       int64_t id)
+  {
+    ResetAnswers();
+
+    if (backend_.listAvailableAttachments(GetContext(), payload_, id) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+        type_ != _OrthancPluginDatabaseAnswerType_Int32)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    target.clear();
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
+    {
+      for (std::list<int32_t>::const_iterator 
+             it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
+      {
+        target.push_back(static_cast<FileContentType>(*it));
+      }
+    }
+  }
+
+
+  void OrthancPluginDatabase::LogChange(int64_t internalId,
+                                        const ServerIndexChange& change)
+  {
+    OrthancPluginChange tmp;
+    tmp.seq = change.GetSeq();
+    tmp.changeType = static_cast<int32_t>(change.GetChangeType());
+    tmp.resourceType = Convert(change.GetResourceType());
+    tmp.publicId = change.GetPublicId().c_str();
+    tmp.date = change.GetDate().c_str();
+
+    if (backend_.logChange(payload_, &tmp) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::LogExportedResource(const ExportedResource& resource)
+  {
+    OrthancPluginExportedResource tmp;
+    tmp.seq = resource.GetSeq();
+    tmp.resourceType = Convert(resource.GetResourceType());
+    tmp.publicId = resource.GetPublicId().c_str();
+    tmp.modality = resource.GetModality().c_str();
+    tmp.date = resource.GetDate().c_str();
+    tmp.patientId = resource.GetPatientId().c_str();
+    tmp.studyInstanceUid = resource.GetStudyInstanceUid().c_str();
+    tmp.seriesInstanceUid = resource.GetSeriesInstanceUid().c_str();
+    tmp.sopInstanceUid = resource.GetSopInstanceUid().c_str();
+
+    if (backend_.logExportedResource(payload_, &tmp) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+    
+  bool OrthancPluginDatabase::LookupAttachment(FileInfo& attachment,
+                                               int64_t id,
+                                               FileContentType contentType)
+  {
+    ResetAnswers();
+
+    if (backend_.lookupAttachment(GetContext(), payload_, id, static_cast<int32_t>(contentType)))
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_None)
+    {
+      return false;
+    }
+    else if (type_ == _OrthancPluginDatabaseAnswerType_Attachment &&
+             answerAttachments_.size() == 1)
+    {
+      attachment = answerAttachments_.front();
+      return true; 
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  bool OrthancPluginDatabase::LookupGlobalProperty(std::string& target,
+                                                   GlobalProperty property)
+  {
+    ResetAnswers();
+
+    if (backend_.lookupGlobalProperty(GetContext(), payload_, 
+                                      static_cast<int32_t>(property)))
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return ForwardSingleAnswer(target);
+  }
+
+
+  void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& target,
+                                               const DicomTag& tag,
+                                               const std::string& value)
+  {
+    ResetAnswers();
+
+    OrthancPluginDicomTag tmp;
+    tmp.group = tag.GetGroup();
+    tmp.element = tag.GetElement();
+    tmp.value = value.c_str();
+
+    if (backend_.lookupIdentifier(GetContext(), payload_, &tmp) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    ForwardAnswers(target);
+  }
+
+
+  void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& target,
+                                               const std::string& value)
+  {
+    ResetAnswers();
+
+    if (backend_.lookupIdentifier2(GetContext(), payload_, value.c_str()) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    ForwardAnswers(target);
+  }
+
+
+  bool OrthancPluginDatabase::LookupMetadata(std::string& target,
+                                             int64_t id,
+                                             MetadataType type)
+  {
+    ResetAnswers();
+
+    if (backend_.lookupMetadata(GetContext(), payload_, id, static_cast<int32_t>(type)))
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return ForwardSingleAnswer(target);
+  }
+
+
+  bool OrthancPluginDatabase::LookupParent(int64_t& parentId,
+                                           int64_t resourceId)
+  {
+    ResetAnswers();
+
+    if (backend_.lookupParent(GetContext(), payload_, resourceId))
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return ForwardSingleAnswer(parentId);
+  }
+
+
+  bool OrthancPluginDatabase::LookupResource(int64_t& id,
+                                             ResourceType& type,
+                                             const std::string& publicId)
+  {
+    ResetAnswers();
+
+    if (backend_.lookupResource(GetContext(), payload_, publicId.c_str()))
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_None)
+    {
+      return false;
+    }
+    else if (type_ == _OrthancPluginDatabaseAnswerType_Resource &&
+             answerResources_.size() == 1)
+    {
+      id = answerResources_.front().first;
+      type = answerResources_.front().second;
+      return true; 
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId)
+  {
+    ResetAnswers();
+
+    if (backend_.selectPatientToRecycle(GetContext(), payload_))
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return ForwardSingleAnswer(internalId);
+  }
+
+
+  bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId,
+                                                     int64_t patientIdToAvoid)
+  {
+    ResetAnswers();
+
+    if (backend_.selectPatientToRecycle2(GetContext(), payload_, patientIdToAvoid))
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    return ForwardSingleAnswer(internalId);
+  }
+
+
+  void OrthancPluginDatabase::SetGlobalProperty(GlobalProperty property,
+                                                const std::string& value)
+  {
+    if (backend_.setGlobalProperty(payload_, static_cast<int32_t>(property), 
+                                   value.c_str()) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::SetMainDicomTag(int64_t id,
+                                              const DicomTag& tag,
+                                              const std::string& value)
+  {
+    int32_t status;
+    OrthancPluginDicomTag tmp;
+    tmp.group = tag.GetGroup();
+    tmp.element = tag.GetElement();
+    tmp.value = value.c_str();
+
+    if (tag.IsIdentifier())
+    {
+      status = backend_.setIdentifierTag(payload_, id, &tmp);
+    }
+    else
+    {
+      status = backend_.setMainDicomTag(payload_, id, &tmp);
+    }
+
+    if (status != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::SetMetadata(int64_t id,
+                                          MetadataType type,
+                                          const std::string& value)
+  {
+    if (backend_.setMetadata(payload_, id, static_cast<int32_t>(type), 
+                             value.c_str()) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::SetProtectedPatient(int64_t internalId, 
+                                                  bool isProtected)
+  {
+    if (backend_.setProtectedPatient(payload_, internalId, isProtected) != 0)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  class OrthancPluginDatabase::Transaction : public SQLite::ITransaction
+  {
+  private:
+    const OrthancPluginDatabaseBackend& backend_;
+    void* payload_;
+
+  public:
+    Transaction(const OrthancPluginDatabaseBackend& backend,
+                void* payload) :
+      backend_(backend),
+      payload_(payload)
+    {
+    }
+
+    virtual void Begin()
+    {
+      if (backend_.startTransaction(payload_) != 0)
+      {
+        throw OrthancException(ErrorCode_Plugin);
+      }
+    }
+
+    virtual void Rollback()
+    {
+      if (backend_.rollbackTransaction(payload_) != 0)
+      {
+        throw OrthancException(ErrorCode_Plugin);
+      }
+    }
+
+    virtual void Commit()
+    {
+      if (backend_.commitTransaction(payload_) != 0)
+      {
+        throw OrthancException(ErrorCode_Plugin);
+      }
+    }
+  };
+
+
+  SQLite::ITransaction* OrthancPluginDatabase::StartTransaction()
+  {
+    return new Transaction(backend_, payload_);
+  }
+
+
+  static void ProcessEvent(IServerIndexListener& listener,
+                           const _OrthancPluginDatabaseAnswer& answer)
+  {
+    switch (answer.type)
+    {
+      case _OrthancPluginDatabaseAnswerType_DeletedAttachment:
+      {
+        const OrthancPluginAttachment& attachment = 
+          *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric);
+        listener.SignalFileDeleted(Convert(attachment));
+        break;
+      }
+        
+      case _OrthancPluginDatabaseAnswerType_RemainingAncestor:
+      {
+        ResourceType type = Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32));
+        listener.SignalRemainingAncestor(type, answer.valueString);
+        break;
+      }
+      
+      case _OrthancPluginDatabaseAnswerType_DeletedResource:
+      {
+        ResourceType type = Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32));
+        ServerIndexChange change(ChangeType_Deleted, type, answer.valueString);
+        listener.SignalChange(change);
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+
+
+  void OrthancPluginDatabase::AnswerReceived(const _OrthancPluginDatabaseAnswer& answer)
+  {
+    if (answer.type == _OrthancPluginDatabaseAnswerType_None)
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    if (answer.type == _OrthancPluginDatabaseAnswerType_DeletedAttachment ||
+        answer.type == _OrthancPluginDatabaseAnswerType_DeletedResource ||
+        answer.type == _OrthancPluginDatabaseAnswerType_RemainingAncestor)
+    {
+      assert(listener_ != NULL);
+      ProcessEvent(*listener_, answer);
+      return;
+    }
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_None)
+    {
+      type_ = answer.type;
+
+      switch (type_)
+      {
+        case _OrthancPluginDatabaseAnswerType_Int32:
+          answerInt32_.clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_Int64:
+          answerInt64_.clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_Resource:
+          answerResources_.clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_Attachment:
+          answerAttachments_.clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_String:
+          answerStrings_.clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_DicomTag:
+          assert(answerDicomMap_ != NULL);
+          answerDicomMap_->Clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_Change:
+          assert(answerChanges_ != NULL);
+          answerChanges_->clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_ExportedResource:
+          assert(answerExportedResources_ != NULL);
+          answerExportedResources_->clear();
+          break;
+
+        default:
+          LOG(ERROR) << "Unhandled type of answer for custom index plugin: " << answer.type;
+          throw OrthancException(ErrorCode_Plugin);
+      }
+    }
+    else if (type_ != answer.type)
+    {
+      LOG(ERROR) << "Error in the plugin protocol: Cannot change the answer type";
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    switch (answer.type)
+    {
+      case _OrthancPluginDatabaseAnswerType_Int32:
+      {
+        answerInt32_.push_back(answer.valueInt32);
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_Int64:
+      {
+        answerInt64_.push_back(answer.valueInt64);
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_Resource:
+      {
+        OrthancPluginResourceType type = static_cast<OrthancPluginResourceType>(answer.valueInt32);
+        answerResources_.push_back(std::make_pair(answer.valueInt64, Convert(type)));
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_Attachment:
+      {
+        const OrthancPluginAttachment& attachment = 
+          *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric);
+
+        answerAttachments_.push_back(Convert(attachment));
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_DicomTag:
+      {
+        const OrthancPluginDicomTag& tag = *reinterpret_cast<const OrthancPluginDicomTag*>(answer.valueGeneric);
+        assert(answerDicomMap_ != NULL);
+        answerDicomMap_->SetValue(tag.group, tag.element, std::string(tag.value));
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_String:
+      {
+        if (answer.valueString == NULL)
+        {
+          throw OrthancException(ErrorCode_Plugin);
+        }
+
+        if (type_ == _OrthancPluginDatabaseAnswerType_None)
+        {
+          type_ = _OrthancPluginDatabaseAnswerType_String;
+          answerStrings_.clear();
+        }
+        else if (type_ != _OrthancPluginDatabaseAnswerType_String)
+        {
+          throw OrthancException(ErrorCode_Plugin);
+        }
+
+        answerStrings_.push_back(std::string(answer.valueString));
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_Change:
+      {
+        assert(answerDone_ != NULL);
+        if (answer.valueUint32 == 1)
+        {
+          *answerDone_ = true;
+        }
+        else if (*answerDone_)
+        {
+          throw OrthancException(ErrorCode_Plugin);
+        }
+        else
+        {
+          const OrthancPluginChange& change = *reinterpret_cast<const OrthancPluginChange*>(answer.valueGeneric);
+          assert(answerChanges_ != NULL);
+          answerChanges_->push_back
+            (ServerIndexChange(change.seq,
+                               static_cast<ChangeType>(change.changeType),
+                               Convert(change.resourceType),
+                               change.publicId,
+                               change.date));                                   
+        }
+
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_ExportedResource:
+      {
+        assert(answerDone_ != NULL);
+        if (answer.valueUint32 == 1)
+        {
+          *answerDone_ = true;
+        }
+        else if (*answerDone_)
+        {
+          throw OrthancException(ErrorCode_Plugin);
+        }
+        else
+        {
+          const OrthancPluginExportedResource& exported = 
+            *reinterpret_cast<const OrthancPluginExportedResource*>(answer.valueGeneric);
+          assert(answerExportedResources_ != NULL);
+          answerExportedResources_->push_back
+            (ExportedResource(exported.seq,
+                              Convert(exported.resourceType),
+                              exported.publicId,
+                              exported.modality,
+                              exported.date,
+                              exported.patientId,
+                              exported.studyInstanceUid,
+                              exported.seriesInstanceUid,
+                              exported.sopInstanceUid));
+        }
+
+        break;
+      }
+
+      default:
+        LOG(ERROR) << "Unhandled type of answer for custom index plugin: " << answer.type;
+        throw OrthancException(ErrorCode_Plugin);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/OrthancPluginDatabase.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,220 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "../../OrthancServer/IDatabaseWrapper.h"
+#include "../Include/OrthancCDatabasePlugin.h"
+
+namespace Orthanc
+{
+  class OrthancPluginDatabase : public IDatabaseWrapper
+  {
+  private:
+    class Transaction;
+
+    typedef std::pair<int64_t, ResourceType>  AnswerResource;
+
+    _OrthancPluginDatabaseAnswerType type_;
+    OrthancPluginDatabaseBackend backend_;
+    void* payload_;
+    IServerIndexListener* listener_;
+
+    std::list<std::string>         answerStrings_;
+    std::list<int32_t>             answerInt32_;
+    std::list<int64_t>             answerInt64_;
+    std::list<AnswerResource>      answerResources_;
+    std::list<FileInfo>            answerAttachments_;
+
+    DicomMap*                      answerDicomMap_;
+    std::list<ServerIndexChange>*  answerChanges_;
+    std::list<ExportedResource>*   answerExportedResources_;
+    bool*                          answerDone_;
+
+    OrthancPluginDatabaseContext* GetContext()
+    {
+      return reinterpret_cast<OrthancPluginDatabaseContext*>(this);
+    }
+
+    void ResetAnswers();
+
+    void ForwardAnswers(std::list<int64_t>& target);
+
+    void ForwardAnswers(std::list<std::string>& target);
+
+    bool ForwardSingleAnswer(std::string& target);
+
+    bool ForwardSingleAnswer(int64_t& target);
+
+  public:
+    OrthancPluginDatabase(const OrthancPluginDatabaseBackend& backend,
+                          void *payload);
+
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment);
+
+    virtual void AttachChild(int64_t parent,
+                             int64_t child);
+
+    virtual void ClearChanges();
+
+    virtual void ClearExportedResources();
+
+    virtual int64_t CreateResource(const std::string& publicId,
+                                   ResourceType type);
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment);
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type);
+
+    virtual void DeleteResource(int64_t id);
+
+    virtual void FlushToDisk()
+    {
+    }
+
+    virtual bool HasFlushToDisk() const
+    {
+      return false;
+    }
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id);
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType);
+
+
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults);
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id);
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id);
+
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults);
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/);
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/);
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id);
+
+    virtual std::string GetPublicId(int64_t resourceId);
+
+    virtual uint64_t GetResourceCount(ResourceType resourceType);
+
+    virtual ResourceType GetResourceType(int64_t resourceId);
+
+    virtual uint64_t GetTotalCompressedSize();
+    
+    virtual uint64_t GetTotalUncompressedSize();
+
+    virtual bool IsExistingResource(int64_t internalId);
+
+    virtual bool IsProtectedPatient(int64_t internalId);
+
+    virtual void ListAvailableMetadata(std::list<MetadataType>& target,
+                                       int64_t id);
+
+    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+                                          int64_t id);
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change);
+
+    virtual void LogExportedResource(const ExportedResource& resource);
+    
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t id,
+                                  FileContentType contentType);
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property);
+
+    virtual void LookupIdentifier(std::list<int64_t>& target,
+                                  const DicomTag& tag,
+                                  const std::string& value);
+
+    virtual void LookupIdentifier(std::list<int64_t>& target,
+                                  const std::string& value);
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type);
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId);
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId);
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId);
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid);
+
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   const std::string& value);
+
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value);
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value);
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected);
+
+    virtual SQLite::ITransaction* StartTransaction();
+
+    virtual void SetListener(IServerIndexListener& listener)
+    {
+      listener_ = &listener;
+    }
+
+    void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/OrthancPlugins.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,1378 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "OrthancPlugins.h"
+
+#include "../../Core/ChunkedBuffer.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
+#include "../../Core/HttpServer/HttpOutput.h"
+#include "../../Core/ImageFormats/PngWriter.h"
+#include "../../OrthancServer/ServerToolbox.h"
+#include "../../OrthancServer/OrthancInitialization.h"
+#include "../../Core/MultiThreading/SharedMessageQueue.h"
+
+#include <boost/thread.hpp>
+#include <boost/regex.hpp> 
+#include <glog/logging.h>
+
+namespace Orthanc
+{
+  static OrthancPluginResourceType Convert(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return OrthancPluginResourceType_Patient;
+
+      case ResourceType_Study:
+        return OrthancPluginResourceType_Study;
+
+      case ResourceType_Series:
+        return OrthancPluginResourceType_Series;
+
+      case ResourceType_Instance:
+        return OrthancPluginResourceType_Instance;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static OrthancPluginChangeType Convert(ChangeType type)
+  {
+    switch (type)
+    {
+      case ChangeType_CompletedSeries:
+        return OrthancPluginChangeType_CompletedSeries;
+
+      case ChangeType_Deleted:
+        return OrthancPluginChangeType_Deleted;
+
+      case ChangeType_NewChildInstance:
+        return OrthancPluginChangeType_NewChildInstance;
+
+      case ChangeType_NewInstance:
+        return OrthancPluginChangeType_NewInstance;
+
+      case ChangeType_NewPatient:
+        return OrthancPluginChangeType_NewPatient;
+
+      case ChangeType_NewSeries:
+        return OrthancPluginChangeType_NewSeries;
+
+      case ChangeType_NewStudy:
+        return OrthancPluginChangeType_NewStudy;
+
+      case ChangeType_StablePatient:
+        return OrthancPluginChangeType_StablePatient;
+
+      case ChangeType_StableSeries:
+        return OrthancPluginChangeType_StableSeries;
+
+      case ChangeType_StableStudy:
+        return OrthancPluginChangeType_StableStudy;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+    class StringHttpOutput : public IHttpOutputStream
+    {
+    private:
+      ChunkedBuffer buffer_;
+
+    public:
+      void GetOutput(std::string& output)
+      {
+        buffer_.Flatten(output);
+      }
+
+      virtual void OnHttpStatusReceived(HttpStatus status)
+      {
+        if (status != HttpStatus_200_Ok)
+        {
+          throw OrthancException(ErrorCode_BadRequest);
+        }
+      }
+
+      virtual void Send(bool isHeader, const void* buffer, size_t length)
+      {
+        if (!isHeader)
+        {
+          buffer_.AddChunk(reinterpret_cast<const char*>(buffer), length);
+        }
+      }
+    };
+
+
+    class PendingChange : public IDynamicObject
+    {
+    private:
+      OrthancPluginChangeType changeType_;
+      OrthancPluginResourceType resourceType_;
+      std::string publicId_;
+
+    public:
+      PendingChange(const ServerIndexChange& change)
+      {
+        changeType_ = Convert(change.GetChangeType());
+        resourceType_ = Convert(change.GetResourceType());
+        publicId_ = change.GetPublicId();
+      }
+
+      void Submit(std::list<OrthancPluginOnChangeCallback>& callbacks)
+      {
+        for (std::list<OrthancPluginOnChangeCallback>::const_iterator 
+               callback = callbacks.begin(); 
+             callback != callbacks.end(); ++callback)
+        {
+          (*callback) (changeType_, resourceType_, publicId_.c_str());
+        }
+      }
+    };
+  }
+
+
+
+  struct OrthancPlugins::PImpl
+  {
+    typedef std::pair<std::string, _OrthancPluginProperty>  Property;
+
+    typedef std::pair<boost::regex*, OrthancPluginRestCallback> RestCallback;
+    typedef std::list<RestCallback>  RestCallbacks;
+    typedef std::list<OrthancPluginOnStoredInstanceCallback>  OnStoredCallbacks;
+    typedef std::list<OrthancPluginOnChangeCallback>  OnChangeCallbacks;
+    typedef std::map<Property, std::string>  Properties;
+
+    ServerContext* context_;
+    RestCallbacks restCallbacks_;
+    OrthancRestApi* restApi_;
+    OnStoredCallbacks  onStoredCallbacks_;
+    OnChangeCallbacks  onChangeCallbacks_;
+    bool hasStorageArea_;
+    _OrthancPluginRegisterStorageArea storageArea_;
+    boost::recursive_mutex callbackMutex_;
+    SharedMessageQueue  pendingChanges_;
+    boost::thread  changeThread_;
+    bool done_;
+    Properties properties_;
+    int argc_;
+    char** argv_;
+    std::auto_ptr<OrthancPluginDatabase>  database_;
+
+    PImpl() : 
+      context_(NULL), 
+      restApi_(NULL),
+      hasStorageArea_(false),
+      done_(false),
+      argc_(1),
+      argv_(NULL)
+    {
+      memset(&storageArea_, 0, sizeof(storageArea_));
+    }
+
+
+    static void ChangeThread(PImpl* that)
+    {
+      while (!that->done_)
+      {
+        std::auto_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(500));
+        
+        if (obj.get() != NULL)
+        {
+          boost::recursive_mutex::scoped_lock lock(that->callbackMutex_);
+          PendingChange& change = *dynamic_cast<PendingChange*>(obj.get());
+          change.Submit(that->onChangeCallbacks_);
+        }
+      }
+    }
+  };
+
+
+  
+  static char* CopyString(const std::string& str)
+  {
+    char *result = reinterpret_cast<char*>(malloc(str.size() + 1));
+    if (result == NULL)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    if (str.size() == 0)
+    {
+      result[0] = '\0';
+    }
+    else
+    {
+      memcpy(result, &str[0], str.size() + 1);
+    }
+
+    return result;
+  }
+
+
+  OrthancPlugins::OrthancPlugins()
+  {
+    pimpl_.reset(new PImpl());
+    pimpl_->changeThread_ = boost::thread(PImpl::ChangeThread, pimpl_.get());
+  }
+
+  
+  void OrthancPlugins::SetServerContext(ServerContext& context)
+  {
+    pimpl_->context_ = &context;
+  }
+
+
+  
+  OrthancPlugins::~OrthancPlugins()
+  {
+    Stop();
+
+    for (PImpl::RestCallbacks::iterator it = pimpl_->restCallbacks_.begin(); 
+         it != pimpl_->restCallbacks_.end(); ++it)
+    {
+      // Delete the regular expression associated with this callback
+      delete it->first;
+    }
+  }
+
+
+  void OrthancPlugins::Stop()
+  {
+    if (!pimpl_->done_)
+    {
+      pimpl_->done_ = true;
+      pimpl_->changeThread_.join();
+    }
+  }
+
+
+
+  static void ArgumentsToPlugin(std::vector<const char*>& keys,
+                                std::vector<const char*>& values,
+                                const HttpHandler::Arguments& arguments)
+  {
+    keys.resize(arguments.size());
+    values.resize(arguments.size());
+
+    size_t pos = 0;
+    for (HttpHandler::Arguments::const_iterator 
+           it = arguments.begin(); it != arguments.end(); ++it)
+    {
+      keys[pos] = it->first.c_str();
+      values[pos] = it->second.c_str();
+      pos++;
+    }
+  }
+
+
+  static void ArgumentsToPlugin(std::vector<const char*>& keys,
+                                std::vector<const char*>& values,
+                                const HttpHandler::GetArguments& arguments)
+  {
+    keys.resize(arguments.size());
+    values.resize(arguments.size());
+
+    for (size_t i = 0; i < arguments.size(); i++)
+    {
+      keys[i] = arguments[i].first.c_str();
+      values[i] = arguments[i].second.c_str();
+    }
+  }
+
+
+  bool OrthancPlugins::Handle(HttpOutput& output,
+                              HttpMethod method,
+                              const UriComponents& uri,
+                              const Arguments& headers,
+                              const GetArguments& getArguments,
+                              const std::string& postData)
+  {
+    std::string flatUri = Toolbox::FlattenUri(uri);
+    OrthancPluginRestCallback callback = NULL;
+
+    std::vector<std::string> groups;
+    std::vector<const char*> cgroups;
+
+    // Loop over the callbacks registered by the plugins
+    bool found = false;
+    for (PImpl::RestCallbacks::const_iterator it = pimpl_->restCallbacks_.begin(); 
+         it != pimpl_->restCallbacks_.end() && !found; ++it)
+    {
+      // Check whether the regular expression associated to this
+      // callback matches the URI
+      boost::cmatch what;
+      if (boost::regex_match(flatUri.c_str(), what, *(it->first)))
+      {
+        callback = it->second;
+
+        // Extract the value of the free parameters of the regular expression
+        if (what.size() > 1)
+        {
+          groups.resize(what.size() - 1);
+          cgroups.resize(what.size() - 1);
+          for (size_t i = 1; i < what.size(); i++)
+          {
+            groups[i - 1] = what[i];
+            cgroups[i - 1] = groups[i - 1].c_str();
+          }
+        }
+
+        found = true;
+      }
+    }
+
+    if (!found)
+    {
+      return false;
+    }
+
+    LOG(INFO) << "Delegating HTTP request to plugin for URI: " << flatUri;
+
+    std::vector<const char*> getKeys, getValues, headersKeys, headersValues;
+
+    OrthancPluginHttpRequest request;
+    memset(&request, 0, sizeof(OrthancPluginHttpRequest));
+
+    ArgumentsToPlugin(headersKeys, headersValues, headers);
+
+    switch (method)
+    {
+      case HttpMethod_Get:
+        request.method = OrthancPluginHttpMethod_Get;
+        ArgumentsToPlugin(getKeys, getValues, getArguments);
+        break;
+
+      case HttpMethod_Post:
+        request.method = OrthancPluginHttpMethod_Post;
+        break;
+
+      case HttpMethod_Delete:
+        request.method = OrthancPluginHttpMethod_Delete;
+        break;
+
+      case HttpMethod_Put:
+        request.method = OrthancPluginHttpMethod_Put;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+
+    request.groups = (cgroups.size() ? &cgroups[0] : NULL);
+    request.groupsCount = cgroups.size();
+    request.getCount = getArguments.size();
+    request.body = (postData.size() ? &postData[0] : NULL);
+    request.bodySize = postData.size();
+    request.headersCount = headers.size();
+    
+    if (getArguments.size() > 0)
+    {
+      request.getKeys = &getKeys[0];
+      request.getValues = &getValues[0];
+    }
+    
+    if (headers.size() > 0)
+    {
+      request.headersKeys = &headersKeys[0];
+      request.headersValues = &headersValues[0];
+    }
+
+    assert(callback != NULL);
+    int32_t error;
+
+    {
+      boost::recursive_mutex::scoped_lock lock(pimpl_->callbackMutex_);
+      error = callback(reinterpret_cast<OrthancPluginRestOutput*>(&output), 
+                       flatUri.c_str(), 
+                       &request);
+    }
+
+    if (error < 0)
+    {
+      LOG(ERROR) << "Plugin callback failed with error code " << error;
+      return false;
+    }
+    else
+    {
+      if (error > 0)
+      {
+        LOG(WARNING) << "Plugin callback finished with warning code " << error;
+      }
+
+      return true;
+    }
+  }
+
+
+  void OrthancPlugins::SignalStoredInstance(DicomInstanceToStore& instance,
+                                            const std::string& instanceId)                                                  
+  {
+    boost::recursive_mutex::scoped_lock lock(pimpl_->callbackMutex_);
+
+    for (PImpl::OnStoredCallbacks::const_iterator
+           callback = pimpl_->onStoredCallbacks_.begin(); 
+         callback != pimpl_->onStoredCallbacks_.end(); ++callback)
+    {
+      (*callback) (reinterpret_cast<OrthancPluginDicomInstance*>(&instance),
+                   instanceId.c_str());
+    }
+  }
+
+
+
+  void OrthancPlugins::SignalChange(const ServerIndexChange& change)
+  {
+    try
+    {
+      pimpl_->pendingChanges_.Enqueue(new PendingChange(change));
+    }
+    catch (OrthancException&)
+    {
+      // This change type or resource type is not supported by the plugin SDK
+      return;
+    }
+  }
+
+
+
+  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
+                                 const void* data,
+                                 size_t size)
+  {
+    target.size = size;
+
+    if (size == 0)
+    {
+      target.data = NULL;
+    }
+    else
+    {
+      target.data = malloc(size);
+      if (target.data != NULL)
+      {
+        memcpy(target.data, data, size);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+    }
+  }
+
+
+  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
+                                 const std::string& str)
+  {
+    if (str.size() == 0)
+    {
+      target.size = 0;
+      target.data = NULL;
+    }
+    else
+    {
+      CopyToMemoryBuffer(target, str.c_str(), str.size());
+    }
+  }
+
+
+  void OrthancPlugins::RegisterRestCallback(const void* parameters)
+  {
+    const _OrthancPluginRestCallback& p = 
+      *reinterpret_cast<const _OrthancPluginRestCallback*>(parameters);
+
+    LOG(INFO) << "Plugin has registered a REST callback on: " << p.pathRegularExpression;
+    pimpl_->restCallbacks_.push_back(std::make_pair(new boost::regex(p.pathRegularExpression), p.callback));
+  }
+
+
+
+  void OrthancPlugins::RegisterOnStoredInstanceCallback(const void* parameters)
+  {
+    const _OrthancPluginOnStoredInstanceCallback& p = 
+      *reinterpret_cast<const _OrthancPluginOnStoredInstanceCallback*>(parameters);
+
+    LOG(INFO) << "Plugin has registered an OnStoredInstance callback";
+    pimpl_->onStoredCallbacks_.push_back(p.callback);
+  }
+
+
+  void OrthancPlugins::RegisterOnChangeCallback(const void* parameters)
+  {
+    const _OrthancPluginOnChangeCallback& p = 
+      *reinterpret_cast<const _OrthancPluginOnChangeCallback*>(parameters);
+
+    LOG(INFO) << "Plugin has registered an OnChange callback";
+    pimpl_->onChangeCallbacks_.push_back(p.callback);
+  }
+
+
+
+  void OrthancPlugins::AnswerBuffer(const void* parameters)
+  {
+    const _OrthancPluginAnswerBuffer& p = 
+      *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters);
+
+    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
+    translatedOutput->SetContentType(p.mimeType);
+    translatedOutput->SendBody(p.answer, p.answerSize);
+  }
+
+
+  void OrthancPlugins::Redirect(const void* parameters)
+  {
+    const _OrthancPluginOutputPlusArgument& p = 
+      *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
+
+    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
+    translatedOutput->Redirect(p.argument);
+  }
+
+
+  void OrthancPlugins::SendHttpStatusCode(const void* parameters)
+  {
+    const _OrthancPluginSendHttpStatusCode& p = 
+      *reinterpret_cast<const _OrthancPluginSendHttpStatusCode*>(parameters);
+
+    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
+    translatedOutput->SendStatus(static_cast<HttpStatus>(p.status));
+  }
+
+
+  void OrthancPlugins::SendUnauthorized(const void* parameters)
+  {
+    const _OrthancPluginOutputPlusArgument& p = 
+      *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
+
+    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
+    translatedOutput->SendUnauthorized(p.argument);
+  }
+
+
+  void OrthancPlugins::SendMethodNotAllowed(const void* parameters)
+  {
+    const _OrthancPluginOutputPlusArgument& p = 
+      *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
+
+    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
+    translatedOutput->SendMethodNotAllowed(p.argument);
+  }
+
+
+  void OrthancPlugins::SetCookie(const void* parameters)
+  {
+    const _OrthancPluginSetHttpHeader& p = 
+      *reinterpret_cast<const _OrthancPluginSetHttpHeader*>(parameters);
+
+    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
+    translatedOutput->SetCookie(p.key, p.value);
+  }
+
+
+  void OrthancPlugins::SetHttpHeader(const void* parameters)
+  {
+    const _OrthancPluginSetHttpHeader& p = 
+      *reinterpret_cast<const _OrthancPluginSetHttpHeader*>(parameters);
+
+    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
+    translatedOutput->AddHeader(p.key, p.value);
+  }
+
+
+  void OrthancPlugins::CompressAndAnswerPngImage(const void* parameters)
+  {
+    const _OrthancPluginCompressAndAnswerPngImage& p = 
+      *reinterpret_cast<const _OrthancPluginCompressAndAnswerPngImage*>(parameters);
+
+    HttpOutput* translatedOutput = reinterpret_cast<HttpOutput*>(p.output);
+
+    PixelFormat format;
+    switch (p.format)
+    {
+      case OrthancPluginPixelFormat_Grayscale8:  
+        format = PixelFormat_Grayscale8;
+        break;
+
+      case OrthancPluginPixelFormat_Grayscale16:  
+        format = PixelFormat_Grayscale16;
+        break;
+
+      case OrthancPluginPixelFormat_SignedGrayscale16:  
+        format = PixelFormat_SignedGrayscale16;
+        break;
+
+      case OrthancPluginPixelFormat_RGB24:  
+        format = PixelFormat_RGB24;
+        break;
+
+      case OrthancPluginPixelFormat_RGBA32:  
+        format = PixelFormat_RGBA32;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    ImageAccessor accessor;
+    accessor.AssignReadOnly(format, p.width, p.height, p.pitch, p.buffer);
+
+    PngWriter writer;
+    std::string png;
+    writer.WriteToMemory(png, accessor);
+
+    translatedOutput->SetContentType("image/png");
+    translatedOutput->SendBody(png);
+  }
+
+
+  void OrthancPlugins::GetDicomForInstance(const void* parameters)
+  {
+    assert(pimpl_->context_ != NULL);
+
+    const _OrthancPluginGetDicomForInstance& p = 
+      *reinterpret_cast<const _OrthancPluginGetDicomForInstance*>(parameters);
+
+    std::string dicom;
+    pimpl_->context_->ReadFile(dicom, p.instanceId, FileContentType_Dicom);
+    CopyToMemoryBuffer(*p.target, dicom);
+  }
+
+
+  void OrthancPlugins::RestApiGet(const void* parameters,
+                                  bool afterPlugins)
+  {
+    const _OrthancPluginRestApiGet& p = 
+      *reinterpret_cast<const _OrthancPluginRestApiGet*>(parameters);
+        
+    HttpHandler::Arguments headers;  // No HTTP header
+    std::string body;  // No body for a GET request
+
+    UriComponents uri;
+    HttpHandler::GetArguments getArguments;
+    HttpHandler::ParseGetQuery(uri, getArguments, p.uri);
+
+    StringHttpOutput stream;
+    HttpOutput http(stream, false /* no keep alive */);
+
+    LOG(INFO) << "Plugin making REST GET call on URI " << p.uri
+              << (afterPlugins ? " (after plugins)" : " (built-in API)");
+
+    bool ok = false;
+    std::string result;
+
+    if (afterPlugins)
+    {
+      ok = Handle(http, HttpMethod_Get, uri, headers, getArguments, body);
+    }
+
+    if (!ok)
+    {
+      ok = (pimpl_->restApi_ != NULL &&
+            pimpl_->restApi_->Handle(http, HttpMethod_Get, uri, headers, getArguments, body));
+    }
+
+    if (ok)
+    {
+      stream.GetOutput(result);
+      CopyToMemoryBuffer(*p.target, result);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+  }
+
+
+  void OrthancPlugins::RestApiPostPut(bool isPost, 
+                                      const void* parameters,
+                                      bool afterPlugins)
+  {
+    const _OrthancPluginRestApiPostPut& p = 
+      *reinterpret_cast<const _OrthancPluginRestApiPostPut*>(parameters);
+
+    HttpHandler::Arguments headers;  // No HTTP header
+    HttpHandler::GetArguments getArguments;  // No GET argument for POST/PUT
+
+    UriComponents uri;
+    Toolbox::SplitUriComponents(uri, p.uri);
+
+    // TODO Avoid unecessary memcpy
+    std::string body(p.body, p.bodySize);
+
+    StringHttpOutput stream;
+    HttpOutput http(stream, false /* no keep alive */);
+
+    HttpMethod method = (isPost ? HttpMethod_Post : HttpMethod_Put);
+    LOG(INFO) << "Plugin making REST " << EnumerationToString(method) << " call on URI " << p.uri
+              << (afterPlugins ? " (after plugins)" : " (built-in API)");
+
+    bool ok = false;
+    std::string result;
+
+    if (afterPlugins)
+    {
+      ok = Handle(http, method, uri, headers, getArguments, body);
+    }
+    
+    if (!ok)
+    {
+      ok = (pimpl_->restApi_ != NULL &&
+            pimpl_->restApi_->Handle(http, method, uri, headers, getArguments, body));
+    }
+
+    if (ok)
+    {
+      stream.GetOutput(result);
+      CopyToMemoryBuffer(*p.target, result);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+  }
+
+
+  void OrthancPlugins::RestApiDelete(const void* parameters,
+                                     bool afterPlugins)
+  {
+    // The "parameters" point to the URI
+    UriComponents uri;
+    Toolbox::SplitUriComponents(uri, reinterpret_cast<const char*>(parameters));
+
+    HttpHandler::Arguments headers;  // No HTTP header
+    HttpHandler::GetArguments getArguments;  // No GET argument for POST/PUT
+    std::string body;  // No body for DELETE
+
+    StringHttpOutput stream;
+    HttpOutput http(stream, false /* no keep alive */);
+
+    LOG(INFO) << "Plugin making REST DELETE call on URI " 
+              << reinterpret_cast<const char*>(parameters)
+              << (afterPlugins ? " (after plugins)" : " (built-in API)");
+
+    bool ok = false;
+
+    if (afterPlugins)
+    {
+      ok = Handle(http, HttpMethod_Delete, uri, headers, getArguments, body);
+    }
+
+    if (!ok)
+    {
+      ok = (pimpl_->restApi_ != NULL &&
+            pimpl_->restApi_->Handle(http, HttpMethod_Delete, uri, headers, getArguments, body));
+    }
+
+    if (!ok)
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+  }
+
+
+  void OrthancPlugins::LookupResource(_OrthancPluginService service,
+                                      const void* parameters)
+  {
+    const _OrthancPluginRetrieveDynamicString& p = 
+      *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters);
+
+    /**
+     * The enumeration below only uses the tags that are indexed in
+     * the Orthanc database. It reflects the
+     * "CandidateResources::ApplyFilter()" method of the
+     * "OrthancFindRequestHandler" class.
+     **/
+
+    DicomTag tag(0, 0);
+    ResourceType level;
+    switch (service)
+    {
+      case _OrthancPluginService_LookupPatient:
+        tag = DICOM_TAG_PATIENT_ID;
+        level = ResourceType_Patient;
+        break;
+
+      case _OrthancPluginService_LookupStudy:
+        tag = DICOM_TAG_STUDY_INSTANCE_UID;
+        level = ResourceType_Study;
+        break;
+
+      case _OrthancPluginService_LookupStudyWithAccessionNumber:
+        tag = DICOM_TAG_ACCESSION_NUMBER;
+        level = ResourceType_Study;
+        break;
+
+      case _OrthancPluginService_LookupSeries:
+        tag = DICOM_TAG_SERIES_INSTANCE_UID;
+        level = ResourceType_Series;
+        break;
+
+      case _OrthancPluginService_LookupInstance:
+        tag = DICOM_TAG_SOP_INSTANCE_UID;
+        level = ResourceType_Instance;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    assert(pimpl_->context_ != NULL);
+
+    std::list<std::string> result;
+    pimpl_->context_->GetIndex().LookupIdentifier(result, tag, p.argument, level);
+
+    if (result.size() == 1)
+    {
+      *p.result = CopyString(result.front());
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  static void AccessInstanceMetadataInternal(bool checkExistence,
+                                             const _OrthancPluginAccessDicomInstance& params,
+                                             const DicomInstanceToStore& instance)
+  {
+    MetadataType metadata;
+
+    try
+    {
+      metadata = StringToMetadata(params.key);
+    }
+    catch (OrthancException&)
+    {
+      // Unknown metadata
+      if (checkExistence)
+      {
+        *params.resultInt64 = -1;
+      }
+      else
+      {
+        *params.resultString = NULL;
+      }
+
+      return;
+    }
+
+    ServerIndex::MetadataMap::const_iterator it = 
+      instance.GetMetadata().find(std::make_pair(ResourceType_Instance, metadata));
+
+    if (checkExistence)
+    {
+      if (it != instance.GetMetadata().end())
+      {
+        *params.resultInt64 = 1;
+      }
+      else
+      {
+        *params.resultInt64 = 0;
+      }
+    }
+    else
+    {
+      if (it != instance.GetMetadata().end())
+      {      
+        *params.resultString = it->second.c_str();
+      }
+      else
+      {
+        // Error: Missing metadata
+        *params.resultString = NULL;
+      }
+    }
+  }
+
+
+  static void AccessDicomInstance(_OrthancPluginService service,
+                                  const void* parameters)
+  {
+    const _OrthancPluginAccessDicomInstance& p = 
+      *reinterpret_cast<const _OrthancPluginAccessDicomInstance*>(parameters);
+
+    DicomInstanceToStore& instance =
+      *reinterpret_cast<DicomInstanceToStore*>(p.instance);
+
+    switch (service)
+    {
+      case _OrthancPluginService_GetInstanceRemoteAet:
+        *p.resultString = instance.GetRemoteAet().c_str();
+        return;
+
+      case _OrthancPluginService_GetInstanceSize:
+        *p.resultInt64 = instance.GetBufferSize();
+        return;
+
+      case _OrthancPluginService_GetInstanceData:
+        *p.resultString = instance.GetBufferData();
+        return;
+
+      case _OrthancPluginService_HasInstanceMetadata:
+        AccessInstanceMetadataInternal(true, p, instance);
+        return;
+
+      case _OrthancPluginService_GetInstanceMetadata:
+        AccessInstanceMetadataInternal(false, p, instance);
+        return;
+
+      case _OrthancPluginService_GetInstanceJson:
+      case _OrthancPluginService_GetInstanceSimplifiedJson:
+      {
+        Json::StyledWriter writer;
+        std::string s;
+
+        if (service == _OrthancPluginService_GetInstanceJson)
+        {
+          s = writer.write(instance.GetJson());
+        }
+        else
+        {
+          Json::Value simplified;
+          SimplifyTags(simplified, instance.GetJson());
+          s = writer.write(simplified);
+        }
+
+        *p.resultStringToFree = CopyString(s);
+        return;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  bool OrthancPlugins::InvokeService(_OrthancPluginService service,
+                                     const void* parameters)
+  {
+    switch (service)
+    {
+      case _OrthancPluginService_GetOrthancPath:
+      {
+        std::string s = Toolbox::GetPathToExecutable();
+        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
+        return true;
+      }
+
+      case _OrthancPluginService_GetOrthancDirectory:
+      {
+        std::string s = Toolbox::GetDirectoryOfExecutable();
+        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
+        return true;
+      }
+
+      case _OrthancPluginService_GetConfigurationPath:
+      {
+        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = 
+          CopyString(Configuration::GetConfigurationAbsolutePath());
+        return true;
+      }
+
+      case _OrthancPluginService_RegisterRestCallback:
+        RegisterRestCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterOnStoredInstanceCallback:
+        RegisterOnStoredInstanceCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterOnChangeCallback:
+        RegisterOnChangeCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_AnswerBuffer:
+        AnswerBuffer(parameters);
+        return true;
+
+      case _OrthancPluginService_CompressAndAnswerPngImage:
+        CompressAndAnswerPngImage(parameters);
+        return true;
+
+      case _OrthancPluginService_GetDicomForInstance:
+        GetDicomForInstance(parameters);
+        return true;
+
+      case _OrthancPluginService_RestApiGet:
+        RestApiGet(parameters, false);
+        return true;
+
+      case _OrthancPluginService_RestApiGetAfterPlugins:
+        RestApiGet(parameters, true);
+        return true;
+
+      case _OrthancPluginService_RestApiPost:
+        RestApiPostPut(true, parameters, false);
+        return true;
+
+      case _OrthancPluginService_RestApiPostAfterPlugins:
+        RestApiPostPut(true, parameters, true);
+        return true;
+
+      case _OrthancPluginService_RestApiDelete:
+        RestApiDelete(parameters, false);
+        return true;
+
+      case _OrthancPluginService_RestApiDeleteAfterPlugins:
+        RestApiDelete(parameters, true);
+        return true;
+
+      case _OrthancPluginService_RestApiPut:
+        RestApiPostPut(false, parameters, false);
+        return true;
+
+      case _OrthancPluginService_RestApiPutAfterPlugins:
+        RestApiPostPut(false, parameters, true);
+        return true;
+
+      case _OrthancPluginService_Redirect:
+        Redirect(parameters);
+        return true;
+
+      case _OrthancPluginService_SendUnauthorized:
+        SendUnauthorized(parameters);
+        return true;
+
+      case _OrthancPluginService_SendMethodNotAllowed:
+        SendMethodNotAllowed(parameters);
+        return true;
+
+      case _OrthancPluginService_SendHttpStatusCode:
+        SendHttpStatusCode(parameters);
+        return true;
+
+      case _OrthancPluginService_SetCookie:
+        SetCookie(parameters);
+        return true;
+
+      case _OrthancPluginService_SetHttpHeader:
+        SetHttpHeader(parameters);
+        return true;
+
+      case _OrthancPluginService_LookupPatient:
+      case _OrthancPluginService_LookupStudy:
+      case _OrthancPluginService_LookupStudyWithAccessionNumber:
+      case _OrthancPluginService_LookupSeries:
+      case _OrthancPluginService_LookupInstance:
+        LookupResource(service, parameters);
+        return true;
+
+      case _OrthancPluginService_GetInstanceRemoteAet:
+      case _OrthancPluginService_GetInstanceSize:
+      case _OrthancPluginService_GetInstanceData:
+      case _OrthancPluginService_GetInstanceJson:
+      case _OrthancPluginService_GetInstanceSimplifiedJson:
+      case _OrthancPluginService_HasInstanceMetadata:
+      case _OrthancPluginService_GetInstanceMetadata:
+        AccessDicomInstance(service, parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterStorageArea:
+      {
+        LOG(INFO) << "Plugin has registered a custom storage area";
+        const _OrthancPluginRegisterStorageArea& p = 
+          *reinterpret_cast<const _OrthancPluginRegisterStorageArea*>(parameters);
+        
+        pimpl_->storageArea_ = p;
+        pimpl_->hasStorageArea_ = true;
+        return true;
+      }
+
+      case _OrthancPluginService_SetPluginProperty:
+      {
+        const _OrthancPluginSetPluginProperty& p = 
+          *reinterpret_cast<const _OrthancPluginSetPluginProperty*>(parameters);
+        pimpl_->properties_[std::make_pair(p.plugin, p.property)] = p.value;
+        return true;
+      }
+
+      case _OrthancPluginService_SetGlobalProperty:
+      {
+        const _OrthancPluginGlobalProperty& p = 
+          *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters);
+        if (p.property < 1024)
+        {
+          return false;
+        }
+        else
+        {
+          assert(pimpl_->context_ != NULL);
+          pimpl_->context_->GetIndex().SetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value);
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_GetGlobalProperty:
+      {
+        assert(pimpl_->context_ != NULL);
+
+        const _OrthancPluginGlobalProperty& p = 
+          *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters);
+        std::string result = pimpl_->context_->GetIndex().GetGlobalProperty(static_cast<GlobalProperty>(p.property), p.value);
+        *(p.result) = CopyString(result);
+        return true;
+      }
+
+      case _OrthancPluginService_GetCommandLineArgumentsCount:
+      {
+        const _OrthancPluginReturnSingleValue& p =
+          *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters);
+        *(p.resultUint32) = pimpl_->argc_ - 1;
+        return true;
+      }
+
+      case _OrthancPluginService_GetCommandLineArgument:
+      {
+        const _OrthancPluginGlobalProperty& p =
+          *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters);
+        
+        if (p.property + 1 > pimpl_->argc_)
+        {
+          return false;
+        }
+        else
+        {
+          std::string arg = std::string(pimpl_->argv_[p.property + 1]);
+          *(p.result) = CopyString(arg);
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_RegisterDatabaseBackend:
+      {
+        LOG(INFO) << "Plugin has registered a custom database back-end";
+        const _OrthancPluginRegisterDatabaseBackend& p =
+          *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackend*>(parameters);
+
+        pimpl_->database_.reset(new OrthancPluginDatabase(*p.backend, p.payload));
+        *(p.result) = reinterpret_cast<OrthancPluginDatabaseContext*>(pimpl_->database_.get());
+
+        return true;
+      }
+
+      case _OrthancPluginService_DatabaseAnswer:
+      {
+        const _OrthancPluginDatabaseAnswer& p =
+          *reinterpret_cast<const _OrthancPluginDatabaseAnswer*>(parameters);
+        if (pimpl_->database_.get() != NULL)
+        {
+          pimpl_->database_->AnswerReceived(p);
+          return true;
+        }
+        else
+        {
+          LOG(ERROR) << "Cannot invoke this service without a custom database back-end";
+          throw OrthancException(ErrorCode_BadRequest);
+        }
+      }
+
+      default:
+        return false;
+    }
+  }
+
+
+  void OrthancPlugins::SetOrthancRestApi(OrthancRestApi& restApi)
+  {
+    pimpl_->restApi_ = &restApi;
+  }
+
+
+  void  OrthancPlugins::ResetOrthancRestApi()
+  {
+    pimpl_->restApi_ = NULL;
+  }
+
+  
+  bool OrthancPlugins::HasStorageArea() const
+  {
+    return pimpl_->hasStorageArea_;
+  }
+  
+  bool OrthancPlugins::HasDatabase() const
+  {
+    return pimpl_->database_.get() != NULL;
+  }
+
+
+
+  namespace
+  {
+    class PluginStorageArea : public IStorageArea
+    {
+    private:
+      _OrthancPluginRegisterStorageArea params_;
+
+      void Free(void* buffer) const
+      {
+        if (buffer != NULL)
+        {
+          params_.free(buffer);
+        }
+      }
+
+      OrthancPluginContentType Convert(FileContentType type) const
+      {
+        switch (type)
+        {
+          case FileContentType_Dicom:
+            return OrthancPluginContentType_Dicom;
+
+          case FileContentType_DicomAsJson:
+            return OrthancPluginContentType_DicomAsJson;
+
+          default:
+            return OrthancPluginContentType_Unknown;
+        }
+      }
+
+    public:
+      PluginStorageArea(const _OrthancPluginRegisterStorageArea& params) : params_(params)
+      {
+      }
+
+      virtual void Create(const std::string& uuid,
+                          const void* content, 
+                          size_t size,
+                          FileContentType type)
+      {
+        if (params_.create(uuid.c_str(), content, size, Convert(type)) != 0)
+        {
+          throw OrthancException(ErrorCode_Plugin);
+        }
+      }
+
+      virtual void Read(std::string& content,
+                        const std::string& uuid,
+                        FileContentType type)
+      {
+        void* buffer = NULL;
+        int64_t size = 0;
+
+        if (params_.read(&buffer, &size, uuid.c_str(), Convert(type)) != 0)
+        {
+          throw OrthancException(ErrorCode_Plugin);
+        }        
+
+        try
+        {
+          content.resize(size);
+        }
+        catch (OrthancException&)
+        {
+          Free(buffer);
+          throw;
+        }
+
+        if (size > 0)
+        {
+          memcpy(&content[0], buffer, size);
+        }
+
+        Free(buffer);
+      }
+
+      virtual void Remove(const std::string& uuid,
+                          FileContentType type) 
+      {
+        if (params_.remove(uuid.c_str(), Convert(type)) != 0)
+        {
+          throw OrthancException(ErrorCode_Plugin);
+        }        
+      }
+    };
+  }
+
+
+  IStorageArea* OrthancPlugins::GetStorageArea()
+  {
+    if (!HasStorageArea())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    return new PluginStorageArea(pimpl_->storageArea_);
+  }
+
+
+  IDatabaseWrapper& OrthancPlugins::GetDatabase()
+  {
+    if (!HasDatabase())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    return *pimpl_->database_;
+  }
+
+
+
+
+
+  const char* OrthancPlugins::GetProperty(const char* plugin,
+                                          _OrthancPluginProperty property) const
+  {
+    PImpl::Property p = std::make_pair(plugin, property);
+    PImpl::Properties::const_iterator it = pimpl_->properties_.find(p);
+
+    if (it == pimpl_->properties_.end())
+    {
+      return NULL;
+    }
+    else
+    {
+      return it->second.c_str();
+    }
+  }
+
+
+  void OrthancPlugins::SetCommandLineArguments(int argc, char* argv[])
+  {
+    if (argc < 1 || argv == NULL)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    pimpl_->argc_ = argc;
+    pimpl_->argv_ = argv;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/OrthancPlugins.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,130 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PluginsManager.h"
+#include "../../Core/HttpServer/HttpHandler.h"
+#include "../../OrthancServer/ServerContext.h"
+#include "../../OrthancServer/OrthancRestApi/OrthancRestApi.h"
+#include "OrthancPluginDatabase.h"
+
+#include <list>
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class OrthancPlugins : public HttpHandler, public IPluginServiceProvider
+  {
+  private:
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    void RegisterRestCallback(const void* parameters);
+
+    void RegisterOnStoredInstanceCallback(const void* parameters);
+
+    void RegisterOnChangeCallback(const void* parameters);
+
+    void AnswerBuffer(const void* parameters);
+
+    void Redirect(const void* parameters);
+
+    void CompressAndAnswerPngImage(const void* parameters);
+
+    void GetDicomForInstance(const void* parameters);
+
+    void RestApiGet(const void* parameters,
+                    bool afterPlugins);
+
+    void RestApiPostPut(bool isPost, 
+                        const void* parameters,
+                        bool afterPlugins);
+
+    void RestApiDelete(const void* parameters,
+                       bool afterPlugins);
+
+    void LookupResource(_OrthancPluginService service,
+                        const void* parameters);
+
+    void SendHttpStatusCode(const void* parameters);
+
+    void SendUnauthorized(const void* parameters);
+
+    void SendMethodNotAllowed(const void* parameters);
+
+    void SetCookie(const void* parameters);
+
+    void SetHttpHeader(const void* parameters);
+
+  public:
+    OrthancPlugins();
+
+    virtual ~OrthancPlugins();
+
+    void SetServerContext(ServerContext& context);
+
+    virtual bool Handle(HttpOutput& output,
+                        HttpMethod method,
+                        const UriComponents& uri,
+                        const Arguments& headers,
+                        const GetArguments& getArguments,
+                        const std::string& postData);
+
+    virtual bool InvokeService(_OrthancPluginService service,
+                               const void* parameters);
+
+    void SignalChange(const ServerIndexChange& change);
+
+    void SignalStoredInstance(DicomInstanceToStore& instance,
+                              const std::string& instanceId);
+
+    void SetOrthancRestApi(OrthancRestApi& restApi);
+
+    void ResetOrthancRestApi();
+
+    bool HasStorageArea() const;
+
+    IStorageArea* GetStorageArea();  // To be freed after use
+
+    bool HasDatabase() const;
+
+    IDatabaseWrapper& GetDatabase();
+
+    void Stop();
+
+    const char* GetProperty(const char* plugin,
+                            _OrthancPluginProperty property) const;
+
+    void SetCommandLineArguments(int argc, char* argv[]);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/PluginsManager.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,344 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PluginsManager.h"
+
+#include "../../Core/Toolbox.h"
+#include "../../Core/HttpServer/HttpOutput.h"
+
+#include <glog/logging.h>
+#include <cassert>
+#include <memory>
+#include <boost/filesystem.hpp>
+
+#ifdef WIN32
+#define PLUGIN_EXTENSION ".dll"
+#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+#define PLUGIN_EXTENSION ".so"
+#elif defined(__APPLE__) && defined(__MACH__)
+#define PLUGIN_EXTENSION ".dylib"
+#else
+#error Support your platform here
+#endif
+
+
+namespace Orthanc
+{
+  static void CallInitialize(SharedLibrary& plugin,
+                             const OrthancPluginContext& context)
+  {
+    typedef int32_t (*Initialize) (const OrthancPluginContext*);
+
+#if defined(_WIN32)
+    Initialize initialize = (Initialize) plugin.GetFunction("OrthancPluginInitialize");
+#else
+    /**
+     * gcc would complain about "ISO C++ forbids casting between
+     * pointer-to-function and pointer-to-object" without the trick
+     * below, that is known as "the POSIX.1-2003 (Technical Corrigendum
+     * 1) workaround". See the man page of "dlsym()".
+     * http://www.trilithium.com/johan/2004/12/problem-with-dlsym/
+     * http://stackoverflow.com/a/14543811/881731
+     **/
+
+    Initialize initialize;
+    *(void **) (&initialize) = plugin.GetFunction("OrthancPluginInitialize");
+#endif
+
+    assert(initialize != NULL);
+    int32_t error = initialize(&context);
+
+    if (error != 0)
+    {
+      LOG(ERROR) << "Error while initializing plugin " << plugin.GetPath()
+                 << " (code " << error << ")";
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+  }
+
+
+  static void CallFinalize(SharedLibrary& plugin)
+  {
+    typedef void (*Finalize) ();
+
+#if defined(_WIN32)
+    Finalize finalize = (Finalize) plugin.GetFunction("OrthancPluginFinalize");
+#else
+    Finalize finalize;
+    *(void **) (&finalize) = plugin.GetFunction("OrthancPluginFinalize");
+#endif
+
+    assert(finalize != NULL);
+    finalize();
+  }
+
+
+  static const char* CallGetName(SharedLibrary& plugin)
+  {
+    typedef const char* (*GetName) ();
+
+#if defined(_WIN32)
+    GetName getName = (GetName) plugin.GetFunction("OrthancPluginGetName");
+#else
+    GetName getName;
+    *(void **) (&getName) = plugin.GetFunction("OrthancPluginGetName");
+#endif
+
+    assert(getName != NULL);
+    return getName();
+  }
+
+
+  static const char* CallGetVersion(SharedLibrary& plugin)
+  {
+    typedef const char* (*GetVersion) ();
+
+#if defined(_WIN32)
+    GetVersion getVersion = (GetVersion) plugin.GetFunction("OrthancPluginGetVersion");
+#else
+    GetVersion getVersion;
+    *(void **) (&getVersion) = plugin.GetFunction("OrthancPluginGetVersion");
+#endif
+
+    assert(getVersion != NULL);
+    return getVersion();
+  }
+
+
+  int32_t PluginsManager::InvokeService(OrthancPluginContext* context,
+                                        _OrthancPluginService service, 
+                                        const void* params)
+  {
+    switch (service)
+    {
+      case _OrthancPluginService_LogError:
+        LOG(ERROR) << reinterpret_cast<const char*>(params);
+        return 0;
+
+      case _OrthancPluginService_LogWarning:
+        LOG(WARNING) << reinterpret_cast<const char*>(params);
+        return 0;
+
+      case _OrthancPluginService_LogInfo:
+        LOG(INFO) << reinterpret_cast<const char*>(params);
+        return 0;
+
+      default:
+        break;
+    }
+
+    PluginsManager* that = reinterpret_cast<PluginsManager*>(context->pluginsManager);
+    bool error = false;
+
+    for (std::list<IPluginServiceProvider*>::iterator
+           it = that->serviceProviders_.begin(); 
+         it != that->serviceProviders_.end(); ++it)
+    {
+      try
+      {
+        if ((*it)->InvokeService(service, params))
+        {
+          return 0;
+        }
+      }
+      catch (OrthancException&)
+      {
+        // This service provider has failed, go to the next
+        error = true;
+      }
+    }
+
+    if (error)
+    {
+      // LOG(ERROR) << "Exception when dealing with service " << service;
+    }
+    else
+    {
+      LOG(ERROR) << "Plugin invoking unknown service " << service;
+    }
+
+    return -1;
+  }
+
+
+  PluginsManager::PluginsManager()
+  {
+    memset(&context_, 0, sizeof(context_));
+    context_.pluginsManager = this;
+    context_.orthancVersion = ORTHANC_VERSION;
+    context_.Free = ::free;
+    context_.InvokeService = InvokeService;
+  }
+
+  PluginsManager::~PluginsManager()
+  {
+    for (Plugins::iterator it = plugins_.begin(); it != plugins_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        LOG(WARNING) << "Unregistering plugin '" << it->first
+                     << "' (version " << it->second->GetVersion() << ")";
+
+        CallFinalize(it->second->GetLibrary());
+        delete it->second;
+      }
+    }
+  }
+
+
+  static bool IsOrthancPlugin(SharedLibrary& library)
+  {
+    return (library.HasFunction("OrthancPluginInitialize") &&
+            library.HasFunction("OrthancPluginFinalize") &&
+            library.HasFunction("OrthancPluginGetName") &&
+            library.HasFunction("OrthancPluginGetVersion"));
+  }
+
+  
+  void PluginsManager::RegisterPlugin(const std::string& path)
+  {
+    if (!boost::filesystem::exists(path))
+    {
+      LOG(ERROR) << "Inexistent path to plugins: " << path;
+      return;
+    }
+
+    if (boost::filesystem::is_directory(path))
+    {
+      ScanFolderForPlugins(path, false);
+      return;
+    }
+
+    std::auto_ptr<Plugin> plugin(new Plugin(path));
+
+    if (!IsOrthancPlugin(plugin->GetLibrary()))
+    {
+      LOG(ERROR) << "Plugin " << plugin->GetLibrary().GetPath()
+                 << " does not declare the proper entry functions";
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+
+    std::string name(CallGetName(plugin->GetLibrary()));
+    if (plugins_.find(name) != plugins_.end())
+    {
+      LOG(ERROR) << "Plugin '" << name << "' already registered";
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+
+    plugin->SetVersion(CallGetVersion(plugin->GetLibrary()));
+    LOG(WARNING) << "Registering plugin '" << name
+                 << "' (version " << plugin->GetVersion() << ")";
+
+    CallInitialize(plugin->GetLibrary(), context_);
+
+    plugins_[name] = plugin.release();
+  }
+
+
+  void PluginsManager::ScanFolderForPlugins(const std::string& folder,
+                                            bool isRecursive)
+  {
+    using namespace boost::filesystem;
+
+    if (!exists(folder))
+    {
+      return;
+    }
+
+    LOG(INFO) << "Scanning folder " << folder << " for plugins";
+
+    directory_iterator end_it; // default construction yields past-the-end
+    for (directory_iterator it(folder);
+          it != end_it;
+          ++it)
+    {
+      std::string path = it->path().string();
+
+      if (is_directory(it->status()))
+      {
+        if (isRecursive)
+        {
+          ScanFolderForPlugins(path, true);
+        }
+      }
+      else
+      {
+        std::string extension = boost::filesystem::extension(it->path());
+        Toolbox::ToLowerCase(extension);
+
+        if (extension == PLUGIN_EXTENSION)
+        {
+          LOG(INFO) << "Found a shared library: " << it->path();
+
+          SharedLibrary plugin(path);
+          if (IsOrthancPlugin(plugin))
+          {
+            RegisterPlugin(path);
+          }
+        }
+      }
+    }
+  }
+
+
+  void PluginsManager::ListPlugins(std::list<std::string>& result) const
+  {
+    result.clear();
+
+    for (Plugins::const_iterator it = plugins_.begin(); 
+         it != plugins_.end(); ++it)
+    {
+      result.push_back(it->first);
+    }
+  }
+
+
+  bool PluginsManager::HasPlugin(const std::string& name) const
+  {
+    return plugins_.find(name) != plugins_.end();
+  }
+
+
+  const std::string& PluginsManager::GetPluginVersion(const std::string& name) const
+  {
+    Plugins::const_iterator it = plugins_.find(name);
+    if (it == plugins_.end())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return it->second->GetVersion();
+    }
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/PluginsManager.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,104 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "SharedLibrary.h"
+#include "IPluginServiceProvider.h"
+
+#include <map>
+#include <list>
+
+namespace Orthanc
+{
+  class PluginsManager : boost::noncopyable
+  {
+  private:
+    class Plugin
+    {
+    private:
+      SharedLibrary library_;
+      std::string  version_;
+
+    public:
+      Plugin(const std::string& path) : library_(path)
+      {
+      }
+
+      SharedLibrary& GetLibrary()
+      {
+        return library_;
+      }
+
+      void SetVersion(const std::string& version)
+      {
+        version_ = version;
+      }
+
+      const std::string& GetVersion() const
+      {
+        return version_;
+      }
+    };
+
+    typedef std::map<std::string, Plugin*>  Plugins;
+
+    OrthancPluginContext  context_;
+    Plugins  plugins_;
+    std::list<IPluginServiceProvider*> serviceProviders_;
+
+    static int32_t InvokeService(OrthancPluginContext* context,
+                                 _OrthancPluginService service,
+                                 const void* parameters);
+
+  public:
+    PluginsManager();
+
+    ~PluginsManager();
+
+    void RegisterPlugin(const std::string& path);
+
+    void ScanFolderForPlugins(const std::string& path,
+                              bool isRecursive);
+
+    void RegisterServiceProvider(IPluginServiceProvider& provider)
+    {
+      serviceProviders_.push_back(&provider);
+    }
+
+    void ListPlugins(std::list<std::string>& result) const;
+
+    bool HasPlugin(const std::string& name) const;
+
+    const std::string& GetPluginVersion(const std::string& name) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/SharedLibrary.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,133 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "SharedLibrary.h"
+
+#include "../../Core/Toolbox.h"
+
+#if defined(_WIN32)
+#include <windows.h>
+#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+#include <dlfcn.h>
+#else
+#error Support your platform here
+#endif
+
+#include <glog/logging.h>
+
+namespace Orthanc
+{
+  SharedLibrary::SharedLibrary(const std::string& path) : 
+    path_(path),
+    handle_(NULL)
+  {
+#if defined(_WIN32)
+    handle_ = ::LoadLibraryA(path.c_str());
+    if (handle_ == NULL)
+    {
+      LOG(ERROR) << "LoadLibrary(" << path << ") failed: Error " << ::GetLastError();
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+
+#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+    handle_ = ::dlopen(path.c_str(), RTLD_NOW);
+    if (handle_ == NULL) 
+    {
+      std::string explanation;
+      const char *tmp = ::dlerror();
+      if (tmp)
+      {
+        explanation = ": Error " + std::string(tmp);
+      }
+
+      LOG(ERROR) << "dlopen(" << path << ") failed" << explanation;
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+
+#else
+#error Support your platform here
+#endif   
+  }
+
+  SharedLibrary::~SharedLibrary()
+  {
+    if (handle_)
+    {
+#if defined(_WIN32)
+      ::FreeLibrary((HMODULE)handle_);
+#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+      ::dlclose(handle_);
+#else
+#error Support your platform here
+#endif
+    }
+  }
+
+
+  SharedLibrary::FunctionPointer SharedLibrary::GetFunctionInternal(const std::string& name)
+  {
+    if (!handle_)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+#if defined(_WIN32)
+    return ::GetProcAddress((HMODULE)handle_, name.c_str());
+#elif defined(__linux) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+    return ::dlsym(handle_, name.c_str());
+#else
+#error Support your platform here
+#endif
+  }
+
+
+  SharedLibrary::FunctionPointer SharedLibrary::GetFunction(const std::string& name)
+  {
+    SharedLibrary::FunctionPointer result = GetFunctionInternal(name);
+  
+    if (result == NULL)
+    {
+      LOG(ERROR) << "Shared library does not expose function \"" << name << "\"";
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  bool SharedLibrary::HasFunction(const std::string& name)
+  {
+    return GetFunctionInternal(name) != NULL;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Engine/SharedLibrary.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,70 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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/OrthancException.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class SharedLibrary : boost::noncopyable
+  {
+  public:
+#if defined(_WIN32)
+    typedef FARPROC FunctionPointer;
+#else
+    typedef void* FunctionPointer;
+#endif
+
+  private:
+    std::string path_;
+    void *handle_;
+
+    FunctionPointer GetFunctionInternal(const std::string& name);
+
+  public:
+    SharedLibrary(const std::string& path);
+
+    ~SharedLibrary();
+
+    const std::string& GetPath() const
+    {
+      return path_;
+    }
+
+    bool HasFunction(const std::string& name);
+
+    FunctionPointer GetFunction(const std::string& name);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Include/OrthancCDatabasePlugin.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,664 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "OrthancCPlugin.h"
+
+
+/** @{ */
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  typedef struct _OrthancPluginDatabaseContext_t OrthancPluginDatabaseContext;
+
+
+  typedef enum
+  {
+    _OrthancPluginDatabaseAnswerType_None = 0,
+
+    /* Events */
+    _OrthancPluginDatabaseAnswerType_DeletedAttachment = 1,
+    _OrthancPluginDatabaseAnswerType_DeletedResource = 2,
+    _OrthancPluginDatabaseAnswerType_RemainingAncestor = 3,
+
+    /* Return value */
+    _OrthancPluginDatabaseAnswerType_Attachment = 10,
+    _OrthancPluginDatabaseAnswerType_Change = 11,
+    _OrthancPluginDatabaseAnswerType_DicomTag = 12,
+    _OrthancPluginDatabaseAnswerType_ExportedResource = 13,
+    _OrthancPluginDatabaseAnswerType_Int32 = 14,
+    _OrthancPluginDatabaseAnswerType_Int64 = 15,
+    _OrthancPluginDatabaseAnswerType_Resource = 16,
+    _OrthancPluginDatabaseAnswerType_String = 17
+  } _OrthancPluginDatabaseAnswerType;
+
+
+  typedef struct
+  {
+    const char* uuid;
+    int32_t     contentType;
+    uint64_t    uncompressedSize;
+    const char* uncompressedHash;
+    int32_t     compressionType;
+    uint64_t    compressedSize;
+    const char* compressedHash;
+  } OrthancPluginAttachment;
+
+  typedef struct
+  {
+    uint16_t     group;
+    uint16_t     element;
+    const char*  value;
+  } OrthancPluginDicomTag;
+
+  typedef struct
+  {
+    int64_t                    seq;
+    int32_t                    changeType;
+    OrthancPluginResourceType  resourceType;
+    const char*                publicId;
+    const char*                date;
+  } OrthancPluginChange;
+
+  typedef struct
+  {
+    int64_t                    seq;
+    OrthancPluginResourceType  resourceType;
+    const char*                publicId;
+    const char*                modality;
+    const char*                date;
+    const char*                patientId;
+    const char*                studyInstanceUid;
+    const char*                seriesInstanceUid;
+    const char*                sopInstanceUid;
+  } OrthancPluginExportedResource;
+
+
+  typedef struct
+  {
+    OrthancPluginDatabaseContext* database;
+    _OrthancPluginDatabaseAnswerType  type;
+    int32_t      valueInt32;
+    uint32_t     valueUint32;
+    int64_t      valueInt64;
+    const char  *valueString;
+    const void  *valueGeneric;
+  } _OrthancPluginDatabaseAnswer;
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerString(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const char*                    value)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_String;
+    params.valueString = value;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerChange(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginChange*     change)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Change;
+    params.valueUint32 = 0;
+    params.valueGeneric = change;
+
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerChangesDone(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Change;
+    params.valueUint32 = 1;
+    params.valueGeneric = NULL;
+
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerInt32(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    int32_t                        value)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Int32;
+    params.valueInt32 = value;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerInt64(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    int64_t                        value)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Int64;
+    params.valueInt64 = value;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerExportedResource(
+    OrthancPluginContext*                 context,
+    OrthancPluginDatabaseContext*         database,
+    const OrthancPluginExportedResource*  exported)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_ExportedResource;
+    params.valueUint32 = 0;
+    params.valueGeneric = exported;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerExportedResourcesDone(
+    OrthancPluginContext*                 context,
+    OrthancPluginDatabaseContext*         database)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_ExportedResource;
+    params.valueUint32 = 1;
+    params.valueGeneric = NULL;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerDicomTag(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginDicomTag*   tag)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_DicomTag;
+    params.valueGeneric = tag;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerAttachment(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginAttachment* attachment)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Attachment;
+    params.valueGeneric = attachment;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerResource(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    int64_t                        id,
+    OrthancPluginResourceType      resourceType)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Resource;
+    params.valueInt64 = id;
+    params.valueInt32 = (int32_t) resourceType;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedAttachment(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginAttachment* attachment)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_DeletedAttachment;
+    params.valueGeneric = attachment;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedResource(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const char*                    publicId,
+    OrthancPluginResourceType      resourceType)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_DeletedResource;
+    params.valueString = publicId;
+    params.valueInt32 = (int32_t) resourceType;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalRemainingAncestor(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const char*                    ancestorId,
+    OrthancPluginResourceType      ancestorType)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_RemainingAncestor;
+    params.valueString = ancestorId;
+    params.valueInt32 = (int32_t) ancestorType;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+
+
+
+
+  typedef struct
+  {
+    int32_t  (*addAttachment) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      const OrthancPluginAttachment* attachment);
+                             
+    int32_t  (*attachChild) (
+      /* inputs */
+      void* payload,
+      int64_t parent,
+      int64_t child);
+                             
+    int32_t  (*clearChanges) (
+      /* inputs */
+      void* payload);
+                             
+    int32_t  (*clearExportedResources) (
+      /* inputs */
+      void* payload);
+
+    int32_t  (*createResource) (
+      /* outputs */
+      int64_t* id, 
+      /* inputs */
+      void* payload,
+      const char* publicId,
+      OrthancPluginResourceType resourceType);           
+                   
+    int32_t  (*deleteAttachment) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t contentType);
+   
+    int32_t  (*deleteMetadata) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t metadataType);
+   
+    int32_t  (*deleteResource) (
+      /* inputs */
+      void* payload,
+      int64_t id);    
+
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    int32_t  (*getAllPublicIds) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      OrthancPluginResourceType resourceType);
+
+    /* Output: Use OrthancPluginDatabaseAnswerChange() and
+     * OrthancPluginDatabaseAnswerChangesDone() */
+    int32_t  (*getChanges) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t since,
+      uint32_t maxResult);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    int32_t  (*getChildrenInternalId) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    int32_t  (*getChildrenPublicId) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    /* Output: Use OrthancPluginDatabaseAnswerExportedResource() and
+     * OrthancPluginDatabaseAnswerExportedResourcesDone() */
+    int32_t  (*getExportedResources) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t  since,
+      uint32_t  maxResult);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerChange() */
+    int32_t  (*getLastChange) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload);
+
+    /* Output: Use OrthancPluginDatabaseAnswerExportedResource() */
+    int32_t  (*getLastExportedResource) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerDicomTag() */
+    int32_t  (*getMainDicomTags) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    int32_t  (*getPublicId) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    int32_t  (*getResourceCount) (
+      /* outputs */
+      uint64_t* target,
+      /* inputs */
+      void* payload,
+      OrthancPluginResourceType  resourceType);
+                   
+    int32_t  (*getResourceType) (
+      /* outputs */
+      OrthancPluginResourceType* resourceType,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    int32_t  (*getTotalCompressedSize) (
+      /* outputs */
+      uint64_t* target,
+      /* inputs */
+      void* payload);
+                   
+    int32_t  (*getTotalUncompressedSize) (
+      /* outputs */
+      uint64_t* target,
+      /* inputs */
+      void* payload);
+                   
+    int32_t  (*isExistingResource) (
+      /* outputs */
+      int32_t* existing,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    int32_t  (*isProtectedPatient) (
+      /* outputs */
+      int32_t* isProtected,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt32() */
+    int32_t  (*listAvailableMetadata) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerInt32() */
+    int32_t  (*listAvailableAttachments) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    int32_t  (*logChange) (
+      /* inputs */
+      void* payload,
+      const OrthancPluginChange* change);
+                   
+    int32_t  (*logExportedResource) (
+      /* inputs */
+      void* payload,
+      const OrthancPluginExportedResource* exported);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerAttachment() */
+    int32_t  (*lookupAttachment) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t contentType);
+
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    int32_t  (*lookupGlobalProperty) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int32_t property);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    int32_t  (*lookupIdentifier) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      const OrthancPluginDicomTag* tag);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    int32_t  (*lookupIdentifier2) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      const char* value);
+
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    int32_t  (*lookupMetadata) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t metadata);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    int32_t  (*lookupParent) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    /* Output: Use OrthancPluginDatabaseAnswerResource() */
+    int32_t  (*lookupResource) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      const char* publicId);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    int32_t  (*selectPatientToRecycle) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    int32_t  (*selectPatientToRecycle2) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t patientIdToAvoid);
+
+    int32_t  (*setGlobalProperty) (
+      /* inputs */
+      void* payload,
+      int32_t property,
+      const char* value);
+
+    int32_t  (*setMainDicomTag) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      const OrthancPluginDicomTag* tag);
+
+    int32_t  (*setIdentifierTag) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      const OrthancPluginDicomTag* tag);
+
+    int32_t  (*setMetadata) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t metadata,
+      const char* value);
+
+    int32_t  (*setProtectedPatient) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t isProtected);
+
+    int32_t (*startTransaction) (
+      /* inputs */
+      void* payload);
+
+    int32_t (*rollbackTransaction) (
+      /* inputs */
+      void* payload);
+
+    int32_t (*commitTransaction) (
+      /* inputs */
+      void* payload);
+
+    int32_t (*open) (
+      /* inputs */
+      void* payload);
+
+    int32_t (*close) (
+      /* inputs */
+      void* payload);
+
+  } OrthancPluginDatabaseBackend;
+
+
+
+  typedef struct
+  {
+    OrthancPluginDatabaseContext**       result;
+    const OrthancPluginDatabaseBackend*  backend;
+    void*                                payload;
+  } _OrthancPluginRegisterDatabaseBackend;
+
+  ORTHANC_PLUGIN_INLINE OrthancPluginDatabaseContext* OrthancPluginRegisterDatabaseBackend(
+    OrthancPluginContext*                context,
+    const OrthancPluginDatabaseBackend*  backend,
+    void*                                payload)
+  {
+    OrthancPluginDatabaseContext* result = NULL;
+
+    _OrthancPluginRegisterDatabaseBackend params;
+    memset(&params, 0, sizeof(params));
+    params.backend = backend;
+    params.result = &result;
+    params.payload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackend, &params) ||
+        result == NULL)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+#ifdef  __cplusplus
+}
+#endif
+
+
+/** @} */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Include/OrthancCPlugin.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,2094 @@
+/**
+ * \mainpage
+ *
+ * This C/C++ SDK allows external developers to create plugins that
+ * can be loaded into Orthanc to extend its functionality. Each
+ * Orthanc plugin must expose 4 public functions with the following
+ * signatures:
+ * 
+ * -# <tt>int32_t OrthancPluginInitialize(const OrthancPluginContext* context)</tt>:
+ *    This function is invoked by Orthanc when it loads the plugin on startup.
+ *    The plugin must:
+ *    - Check its compatibility with the Orthanc version using
+ *      ::OrthancPluginCheckVersion().
+ *    - Store the context pointer so that it can use the plugin 
+ *      services of Orthanc.
+ *    - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback().
+ *    - Register all its callbacks for received instances using ::OrthancPluginRegisterOnStoredInstanceCallback().
+ *    - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea().
+ *    - Possibly register a custom database back-end area using ::OrthancPluginRegisterDatabaseBackend().
+ * -# <tt>void OrthancPluginFinalize()</tt>:
+ *    This function is invoked by Orthanc during its shutdown. The plugin
+ *    must free all its memory.
+ * -# <tt>const char* OrthancPluginGetName()</tt>:
+ *    The plugin must return a short string to identify itself.
+ * -# <tt>const char* OrthancPluginGetVersion()</tt>:
+ *    The plugin must return a string containing its version number.
+ *
+ * The name and the version of a plugin is only used to prevent it
+ * from being loaded twice.
+ * 
+ * The various callbacks are guaranteed to be executed in mutual
+ * exclusion since Orthanc 0.8.5.
+ **/
+
+
+
+/**
+ * @defgroup CInterface C Interface 
+ * @brief The C interface to create Orthanc plugins.
+ * 
+ * These functions must be used to create C plugins for Orthanc.
+ **/
+
+
+
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 <stdio.h>
+#include <string.h>
+
+#ifdef WIN32
+#define ORTHANC_PLUGINS_API __declspec(dllexport)
+#else
+#define ORTHANC_PLUGINS_API
+#endif
+
+#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     0
+#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     8
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  6
+
+
+
+/********************************************************************
+ ** Check that function inlining is properly supported. The use of
+ ** inlining is required, to avoid the duplication of object code
+ ** between two compilation modules that would use the Orthanc Plugin
+ ** API.
+ ********************************************************************/
+
+/* If the auto-detection of the "inline" keyword below does not work
+   automatically and that your compiler is known to properly support
+   inlining, uncomment the following #define and adapt the definition
+   of "static inline". */
+
+/* #define ORTHANC_PLUGIN_INLINE static inline */
+
+#ifndef ORTHANC_PLUGIN_INLINE
+#  if __STDC_VERSION__ >= 199901L
+/*   This is C99 or above: http://predef.sourceforge.net/prestd.html */
+#    define ORTHANC_PLUGIN_INLINE static inline
+#  elif defined(__cplusplus)
+/*   This is C++ */
+#    define ORTHANC_PLUGIN_INLINE static inline
+#  elif defined(__GNUC__)
+/*   This is GCC running in C89 mode */
+#    define ORTHANC_PLUGIN_INLINE static __inline
+#  elif defined(_MSC_VER)
+/*   This is Visual Studio running in C89 mode */
+#    define ORTHANC_PLUGIN_INLINE static __inline
+#  else
+#    error Your compiler is not known to support the "inline" keyword
+#  endif
+#endif
+
+
+
+/********************************************************************
+ ** Inclusion of standard libraries.
+ ********************************************************************/
+
+/**
+ * For Microsoft Visual Studio, a compatibility "stdint.h" can be
+ * downloaded at the following URL:
+ * https://orthanc.googlecode.com/hg/Resources/ThirdParty/VisualStudio/stdint.h
+ **/
+#include <stdint.h>
+
+#include <stdlib.h>
+
+
+
+/********************************************************************
+ ** Definition of the Orthanc Plugin API.
+ ********************************************************************/
+
+/** @{ */
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * Forward declaration of one of the mandatory functions for Orthanc
+   * plugins.
+   **/
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName();
+
+
+  /**
+   * The various HTTP methods for a REST call.
+   **/
+  typedef enum
+  {
+    OrthancPluginHttpMethod_Get = 1,    /*!< GET request */
+    OrthancPluginHttpMethod_Post = 2,   /*!< POST request */
+    OrthancPluginHttpMethod_Put = 3,    /*!< PUT request */
+    OrthancPluginHttpMethod_Delete = 4  /*!< DELETE request */
+  } OrthancPluginHttpMethod;
+
+
+  /**
+   * @brief The parameters of a REST request.
+   **/
+  typedef struct
+  {
+    /**
+     * @brief The HTTP method.
+     **/
+    OrthancPluginHttpMethod method;    
+
+    /**
+     * @brief The number of groups of the regular expression.
+     **/
+    uint32_t                groupsCount;
+
+    /**
+     * @brief The matched values for the groups of the regular expression.
+     **/
+    const char* const*      groups;
+
+    /**
+     * @brief For a GET request, the number of GET parameters.
+     **/
+    uint32_t                getCount;
+
+    /**
+     * @brief For a GET request, the keys of the GET parameters.
+     **/
+    const char* const*      getKeys;
+
+    /**
+     * @brief For a GET request, the values of the GET parameters.
+     **/
+    const char* const*      getValues;
+
+    /**
+     * @brief For a PUT or POST request, the content of the body.
+     **/
+    const char*             body;
+
+    /**
+     * @brief For a PUT or POST request, the number of bytes of the body.
+     **/
+    uint32_t                bodySize;
+
+
+    /* --------------------------------------------------
+       New in version 0.8.1
+       -------------------------------------------------- */
+
+    /**
+     * @brief The number of HTTP headers.
+     **/
+    uint32_t                headersCount;
+
+    /**
+     * @brief The keys of the HTTP headers (always converted to low-case).
+     **/
+    const char* const*      headersKeys;
+
+    /**
+     * @brief The values of the HTTP headers.
+     **/
+    const char* const*      headersValues;
+
+  } OrthancPluginHttpRequest;
+
+
+  typedef enum 
+  {
+    /* Generic services */
+    _OrthancPluginService_LogInfo = 1,
+    _OrthancPluginService_LogWarning = 2,
+    _OrthancPluginService_LogError = 3,
+    _OrthancPluginService_GetOrthancPath = 4,
+    _OrthancPluginService_GetOrthancDirectory = 5,
+    _OrthancPluginService_GetConfigurationPath = 6,
+    _OrthancPluginService_SetPluginProperty = 7,
+    _OrthancPluginService_GetGlobalProperty = 8,
+    _OrthancPluginService_SetGlobalProperty = 9,
+    _OrthancPluginService_GetCommandLineArgumentsCount = 10,
+    _OrthancPluginService_GetCommandLineArgument = 11,
+
+    /* Registration of callbacks */
+    _OrthancPluginService_RegisterRestCallback = 1000,
+    _OrthancPluginService_RegisterOnStoredInstanceCallback = 1001,
+    _OrthancPluginService_RegisterStorageArea = 1002,
+    _OrthancPluginService_RegisterOnChangeCallback = 1003,
+
+    /* Sending answers to REST calls */
+    _OrthancPluginService_AnswerBuffer = 2000,
+    _OrthancPluginService_CompressAndAnswerPngImage = 2001,
+    _OrthancPluginService_Redirect = 2002,
+    _OrthancPluginService_SendHttpStatusCode = 2003,
+    _OrthancPluginService_SendUnauthorized = 2004,
+    _OrthancPluginService_SendMethodNotAllowed = 2005,
+    _OrthancPluginService_SetCookie = 2006,
+    _OrthancPluginService_SetHttpHeader = 2007,
+
+    /* Access to the Orthanc database and API */
+    _OrthancPluginService_GetDicomForInstance = 3000,
+    _OrthancPluginService_RestApiGet = 3001,
+    _OrthancPluginService_RestApiPost = 3002,
+    _OrthancPluginService_RestApiDelete = 3003,
+    _OrthancPluginService_RestApiPut = 3004,
+    _OrthancPluginService_LookupPatient = 3005,
+    _OrthancPluginService_LookupStudy = 3006,
+    _OrthancPluginService_LookupSeries = 3007,
+    _OrthancPluginService_LookupInstance = 3008,
+    _OrthancPluginService_LookupStudyWithAccessionNumber = 3009,
+    _OrthancPluginService_RestApiGetAfterPlugins = 3010,
+    _OrthancPluginService_RestApiPostAfterPlugins = 3011,
+    _OrthancPluginService_RestApiDeleteAfterPlugins = 3012,
+    _OrthancPluginService_RestApiPutAfterPlugins = 3013,
+
+    /* Access to DICOM instances */
+    _OrthancPluginService_GetInstanceRemoteAet = 4000,
+    _OrthancPluginService_GetInstanceSize = 4001,
+    _OrthancPluginService_GetInstanceData = 4002,
+    _OrthancPluginService_GetInstanceJson = 4003,
+    _OrthancPluginService_GetInstanceSimplifiedJson = 4004,
+    _OrthancPluginService_HasInstanceMetadata = 4005,
+    _OrthancPluginService_GetInstanceMetadata = 4006,
+
+    /* Services for plugins implementing a database back-end */
+    _OrthancPluginService_RegisterDatabaseBackend = 5000,
+    _OrthancPluginService_DatabaseAnswer = 5001
+
+  } _OrthancPluginService;
+
+
+  typedef enum
+  {
+    _OrthancPluginProperty_Description = 1,
+    _OrthancPluginProperty_RootUri = 2,
+    _OrthancPluginProperty_OrthancExplorer = 3
+  } _OrthancPluginProperty;
+
+
+
+  /**
+   * The memory layout of the pixels of an image.
+   **/
+  typedef enum
+  {
+    /**
+     * @brief Graylevel 8bpp image.
+     *
+     * The image is graylevel. Each pixel is unsigned and stored in
+     * one byte.
+     **/
+    OrthancPluginPixelFormat_Grayscale8 = 1,
+
+    /**
+     * @brief Graylevel, unsigned 16bpp image.
+     *
+     * The image is graylevel. Each pixel is unsigned and stored in
+     * two bytes.
+     **/
+    OrthancPluginPixelFormat_Grayscale16 = 2,
+
+    /**
+     * @brief Graylevel, signed 16bpp image.
+     *
+     * The image is graylevel. Each pixel is signed and stored in two
+     * bytes.
+     **/
+    OrthancPluginPixelFormat_SignedGrayscale16 = 3,
+
+    /**
+     * @brief Color image in RGB24 format.
+     *
+     * This format describes a color image. The pixels are stored in 3
+     * consecutive bytes. The memory layout is RGB.
+     **/
+    OrthancPluginPixelFormat_RGB24 = 4,
+
+    /**
+     * @brief Color image in RGBA32 format.
+     *
+     * This format describes a color image. The pixels are stored in 4
+     * consecutive bytes. The memory layout is RGBA.
+     **/
+    OrthancPluginPixelFormat_RGBA32 = 5
+  } OrthancPluginPixelFormat;
+
+
+
+  /**
+   * The content types that are supported by Orthanc plugins.
+   **/
+  typedef enum
+  {
+    OrthancPluginContentType_Unknown = 0,      /*!< Unknown content type */
+    OrthancPluginContentType_Dicom = 1,        /*!< DICOM */
+    OrthancPluginContentType_DicomAsJson = 2   /*!< JSON summary of a DICOM file */
+  } OrthancPluginContentType;
+
+
+
+  /**
+   * The supported type of DICOM resources.
+   **/
+  typedef enum
+  {
+    OrthancPluginResourceType_Patient = 0,     /*!< Patient */
+    OrthancPluginResourceType_Study = 1,       /*!< Study */
+    OrthancPluginResourceType_Series = 2,      /*!< Series */
+    OrthancPluginResourceType_Instance = 3     /*!< Instance */
+  } OrthancPluginResourceType;
+
+
+
+  /**
+   * The supported type of changes that can happen to DICOM resources.
+   **/
+  typedef enum
+  {
+    OrthancPluginChangeType_CompletedSeries = 0,    /*!< Series is now complete */
+    OrthancPluginChangeType_Deleted = 1,            /*!< Deleted resource */
+    OrthancPluginChangeType_NewChildInstance = 2,   /*!< A new instance was added to this resource */
+    OrthancPluginChangeType_NewInstance = 3,        /*!< New instance received */
+    OrthancPluginChangeType_NewPatient = 4,         /*!< New patient created */
+    OrthancPluginChangeType_NewSeries = 5,          /*!< New series created */
+    OrthancPluginChangeType_NewStudy = 6,           /*!< New study created */
+    OrthancPluginChangeType_StablePatient = 7,      /*!< Timeout: No new instance in this patient */
+    OrthancPluginChangeType_StableSeries = 8,       /*!< Timeout: No new instance in this series */
+    OrthancPluginChangeType_StableStudy = 9         /*!< Timeout: No new instance in this study */
+  } OrthancPluginChangeType;
+
+
+
+  /**
+   * @brief A memory buffer allocated by the core system of Orthanc.
+   *
+   * A memory buffer allocated by the core system of Orthanc. When the
+   * content of the buffer is not useful anymore, it must be free by a
+   * call to ::OrthancPluginFreeMemoryBuffer().
+   **/
+  typedef struct
+  {
+    /**
+     * @brief The content of the buffer.
+     **/
+    void*      data;
+
+    /**
+     * @brief The number of bytes in the buffer.
+     **/
+    uint32_t   size;
+  } OrthancPluginMemoryBuffer;
+
+
+
+
+  /**
+   * @brief Opaque structure that represents the HTTP connection to the client application.
+   **/
+  typedef struct _OrthancPluginRestOutput_t OrthancPluginRestOutput;
+
+
+
+  /**
+   * @brief Opaque structure that represents a DICOM instance received by Orthanc.
+   **/
+  typedef struct _OrthancPluginDicomInstance_t OrthancPluginDicomInstance;
+
+
+
+  /**
+   * @brief Signature of a callback function that answers to a REST request.
+   **/
+  typedef int32_t (*OrthancPluginRestCallback) (
+    OrthancPluginRestOutput* output,
+    const char* url,
+    const OrthancPluginHttpRequest* request);
+
+
+
+  /**
+   * @brief Signature of a callback function that is triggered when Orthanc receives a DICOM instance.
+   **/
+  typedef int32_t (*OrthancPluginOnStoredInstanceCallback) (
+    OrthancPluginDicomInstance* instance,
+    const char* instanceId);
+
+
+
+  /**
+   * @brief Signature of a callback function that is triggered when a change happens to some DICOM resource.
+   **/
+  typedef int32_t (*OrthancPluginOnChangeCallback) (
+    OrthancPluginChangeType changeType,
+    OrthancPluginResourceType resourceType,
+    const char* resourceId);
+
+
+
+  /**
+   * @brief Signature of a function to free dynamic memory.
+   **/
+  typedef void (*OrthancPluginFree) (void* buffer);
+
+
+
+  /**
+   * @brief Callback for writing to the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
+   *
+   * @param uuid The UUID of the file.
+   * @param content The content of the file.
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   **/
+  typedef int32_t (*OrthancPluginStorageCreate) (
+    const char* uuid,
+    const void* content,
+    int64_t size,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for reading from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc reads a file from the storage area.
+   *
+   * @param content The content of the file (output).
+   * @param size The size of the file (output).
+   * @param uuid The UUID of the file of interest.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   **/
+  typedef int32_t (*OrthancPluginStorageRead) (
+    void** content,
+    int64_t* size,
+    const char* uuid,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for removing a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc deletes a file from the storage area.
+   *
+   * @param uuid The UUID of the file to be removed.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   **/
+  typedef int32_t (*OrthancPluginStorageRemove) (
+    const char* uuid,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Data structure that contains information about the Orthanc core.
+   **/
+  typedef struct _OrthancPluginContext_t
+  {
+    void*              pluginsManager;
+    const char*        orthancVersion;
+    OrthancPluginFree  Free;
+    int32_t          (*InvokeService) (struct _OrthancPluginContext_t* context,
+                                       _OrthancPluginService service,
+                                       const void* params);
+  } OrthancPluginContext;
+
+
+
+  /**
+   * @brief Free a string.
+   * 
+   * Free a string that was allocated by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param str The string to be freed.
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeString(
+    OrthancPluginContext* context, 
+    char* str)
+  {
+    if (str != NULL)
+    {
+      context->Free(str);
+    }
+  }
+
+
+  /**
+   * @brief Check the compatibility of the plugin wrt. the version of its hosting Orthanc.
+   * 
+   * This function checks whether the version of this C header is
+   * compatible with the current version of Orthanc. The result of
+   * this function should always be checked in the
+   * OrthancPluginInitialize() entry point of the plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return 1 if and only if the versions are compatible. If the
+   * result is 0, the initialization of the plugin should fail.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginCheckVersion(
+    OrthancPluginContext* context)
+  {
+    int major, minor, revision;
+
+    /* Assume compatibility with the mainline */
+    if (!strcmp(context->orthancVersion, "mainline"))
+    {
+      return 1;
+    }
+
+    /* Parse the version of the Orthanc core */
+    if ( 
+#ifdef _MSC_VER
+      sscanf_s
+#else
+      sscanf
+#endif
+      (context->orthancVersion, "%4d.%4d.%4d", &major, &minor, &revision) != 3)
+    {
+      return 0;
+    }
+
+    /* Check the major number of the version */
+
+    if (major > ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER)
+    {
+      return 1;
+    }
+
+    if (major < ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER)
+    {
+      return 0;
+    }
+
+    /* Check the minor number of the version */
+
+    if (minor > ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER)
+    {
+      return 1;
+    }
+
+    if (minor < ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER)
+    {
+      return 0;
+    }
+
+    /* Check the revision number of the version */
+
+    if (revision >= ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER)
+    {
+      return 1;
+    }
+    else
+    {
+      return 0;
+    }
+  }
+
+
+  /**
+   * @brief Free a memory buffer.
+   * 
+   * Free a memory buffer that was allocated by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer to release.
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeMemoryBuffer(
+    OrthancPluginContext* context, 
+    OrthancPluginMemoryBuffer* buffer)
+  {
+    context->Free(buffer->data);
+  }
+
+
+  /**
+   * @brief Log an error.
+   *
+   * Log an error message using the Orthanc logging system.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogError(
+    OrthancPluginContext* context,
+    const char* message)
+  {
+    context->InvokeService(context, _OrthancPluginService_LogError, message);
+  }
+
+
+  /**
+   * @brief Log a warning.
+   *
+   * Log a warning message using the Orthanc logging system.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogWarning(
+    OrthancPluginContext* context,
+    const char* message)
+  {
+    context->InvokeService(context, _OrthancPluginService_LogWarning, message);
+  }
+
+
+  /**
+   * @brief Log an information.
+   *
+   * Log an information message using the Orthanc logging system.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogInfo(
+    OrthancPluginContext* context,
+    const char* message)
+  {
+    context->InvokeService(context, _OrthancPluginService_LogInfo, message);
+  }
+
+
+
+  typedef struct
+  {
+    const char* pathRegularExpression;
+    OrthancPluginRestCallback callback;
+  } _OrthancPluginRestCallback;
+
+  /**
+   * @brief Register a REST callback.
+   *
+   * This function registers a REST callback against a regular
+   * expression for a URI. This function must be called during the
+   * initialization of the plugin, i.e. inside the
+   * OrthancPluginInitialize() public function.
+   * 
+   * @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.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallback(
+    OrthancPluginContext*     context,
+    const char*               pathRegularExpression,
+    OrthancPluginRestCallback callback)
+  {
+    _OrthancPluginRestCallback params;
+    params.pathRegularExpression = pathRegularExpression;
+    params.callback = callback;
+    context->InvokeService(context, _OrthancPluginService_RegisterRestCallback, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginOnStoredInstanceCallback callback;
+  } _OrthancPluginOnStoredInstanceCallback;
+
+  /**
+   * @brief Register a callback for received instances.
+   *
+   * This function registers a callback function that is called
+   * whenever a new DICOM instance is stored into the Orthanc core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback function.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnStoredInstanceCallback(
+    OrthancPluginContext*                  context,
+    OrthancPluginOnStoredInstanceCallback  callback)
+  {
+    _OrthancPluginOnStoredInstanceCallback params;
+    params.callback = callback;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterOnStoredInstanceCallback, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              answer;
+    uint32_t                 answerSize;
+    const char*              mimeType;
+  } _OrthancPluginAnswerBuffer;
+
+  /**
+   * @brief Answer to a REST request.
+   *
+   * This function answers to a REST request with the content of a memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param answer Pointer to the memory buffer containing the answer.
+   * @param answerSize Number of bytes of the answer.
+   * @param mimeType The MIME type of the answer.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginAnswerBuffer(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              answer,
+    uint32_t                 answerSize,
+    const char*              mimeType)
+  {
+    _OrthancPluginAnswerBuffer params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.mimeType = mimeType;
+    context->InvokeService(context, _OrthancPluginService_AnswerBuffer, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput*  output;
+    OrthancPluginPixelFormat  format;
+    uint32_t                  width;
+    uint32_t                  height;
+    uint32_t                  pitch;
+    const void*               buffer;
+  } _OrthancPluginCompressAndAnswerPngImage;
+
+  /**
+   * @brief Answer to a REST request with a PNG image.
+   *
+   * This function answers to a REST request with a PNG image. The
+   * parameters of this function describe a memory buffer that
+   * contains an uncompressed image. The image will be automatically compressed
+   * as a PNG image by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param format The memory layout of the uncompressed image.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @param pitch The pitch of the image (i.e. the number of bytes
+   * between 2 successive lines of the image in the memory buffer.
+   * @param buffer The memory buffer containing the uncompressed image.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginCompressAndAnswerPngImage(
+    OrthancPluginContext*     context,
+    OrthancPluginRestOutput*  output,
+    OrthancPluginPixelFormat  format,
+    uint32_t                  width,
+    uint32_t                  height,
+    uint32_t                  pitch,
+    const void*               buffer)
+  {
+    _OrthancPluginCompressAndAnswerPngImage params;
+    params.output = output;
+    params.format = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+    context->InvokeService(context, _OrthancPluginService_CompressAndAnswerPngImage, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 instanceId;
+  } _OrthancPluginGetDicomForInstance;
+
+  /**
+   * @brief Retrieve a DICOM instance using its Orthanc identifier.
+   * 
+   * Retrieve a DICOM instance using its Orthanc identifier. The DICOM
+   * file is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer.
+   * @param instanceId The Orthanc identifier of the DICOM instance of interest.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginGetDicomForInstance(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 instanceId)
+  {
+    _OrthancPluginGetDicomForInstance params;
+    params.target = target;
+    params.instanceId = instanceId;
+    return context->InvokeService(context, _OrthancPluginService_GetDicomForInstance, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 uri;
+  } _OrthancPluginRestApiGet;
+
+  /**
+   * @brief Make a GET call to the built-in Orthanc REST API.
+   * 
+   * Make a GET call to the built-in Orthanc REST API. The result to
+   * the query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer.
+   * @param uri The URI in the built-in Orthanc API.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginRestApiGet(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri)
+  {
+    _OrthancPluginRestApiGet params;
+    params.target = target;
+    params.uri = uri;
+    return context->InvokeService(context, _OrthancPluginService_RestApiGet, &params);
+  }
+
+
+
+  /**
+   * @brief Make a GET call to the REST API, as tainted by the plugins.
+   * 
+   * Make a GET call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. The result to the
+   * query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer.
+   * @param uri The URI in the built-in Orthanc API.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginRestApiGetAfterPlugins(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri)
+  {
+    _OrthancPluginRestApiGet params;
+    params.target = target;
+    params.uri = uri;
+    return context->InvokeService(context, _OrthancPluginService_RestApiGetAfterPlugins, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 uri;
+    const char*                 body;
+    uint32_t                    bodySize;
+  } _OrthancPluginRestApiPostPut;
+
+  /**
+   * @brief Make a POST call to the built-in Orthanc REST API.
+   * 
+   * Make a POST call to the built-in Orthanc REST API. The result to
+   * the query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer.
+   * @param uri The URI in the built-in Orthanc API.
+   * @param body The body of the POST request.
+   * @param bodySize The size of the body.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginRestApiPost(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const char*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPost, &params);
+  }
+
+
+  /**
+   * @brief Make a POST call to the REST API, as tainted by the plugins.
+   * 
+   * Make a POST call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. The result to the
+   * query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer.
+   * @param uri The URI in the built-in Orthanc API.
+   * @param body The body of the POST request.
+   * @param bodySize The size of the body.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginRestApiPostAfterPlugins(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const char*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPostAfterPlugins, &params);
+  }
+
+
+
+  /**
+   * @brief Make a DELETE call to the built-in Orthanc REST API.
+   * 
+   * Make a DELETE call to the built-in Orthanc REST API.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param uri The URI to delete in the built-in Orthanc API.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginRestApiDelete(
+    OrthancPluginContext*       context,
+    const char*                 uri)
+  {
+    return context->InvokeService(context, _OrthancPluginService_RestApiDelete, uri);
+  }
+
+
+  /**
+   * @brief Make a DELETE call to the REST API, as tainted by the plugins.
+   * 
+   * Make a DELETE call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. 
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param uri The URI to delete in the built-in Orthanc API.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginRestApiDeleteAfterPlugins(
+    OrthancPluginContext*       context,
+    const char*                 uri)
+  {
+    return context->InvokeService(context, _OrthancPluginService_RestApiDeleteAfterPlugins, uri);
+  }
+
+
+
+  /**
+   * @brief Make a PUT call to the built-in Orthanc REST API.
+   * 
+   * Make a PUT call to the built-in Orthanc REST API. The result to
+   * the query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer.
+   * @param uri The URI in the built-in Orthanc API.
+   * @param body The body of the PUT request.
+   * @param bodySize The size of the body.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginRestApiPut(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const char*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPut, &params);
+  }
+
+
+
+  /**
+   * @brief Make a PUT call to the REST API, as tainted by the plugins.
+   * 
+   * Make a PUT call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. The result to the
+   * query is stored into a newly allocated memory buffer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer.
+   * @param uri The URI in the built-in Orthanc API.
+   * @param body The body of the PUT request.
+   * @param bodySize The size of the body.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginRestApiPutAfterPlugins(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const char*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPutAfterPlugins, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              argument;
+  } _OrthancPluginOutputPlusArgument;
+
+  /**
+   * @brief Redirect a REST request.
+   *
+   * This function answers to a REST request by redirecting the user
+   * to another URI using HTTP status 301.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param redirection Where to redirect.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRedirect(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              redirection)
+  {
+    _OrthancPluginOutputPlusArgument params;
+    params.output = output;
+    params.argument = redirection;
+    context->InvokeService(context, _OrthancPluginService_Redirect, &params);
+  }
+
+
+
+  typedef struct
+  {
+    char**       result;
+    const char*  argument;
+  } _OrthancPluginRetrieveDynamicString;
+
+  /**
+   * @brief Look for a patient.
+   *
+   * Look for a patient stored in Orthanc, using its Patient ID tag (0x0010, 0x0020).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored patients).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param patientID The Patient ID of interest.
+   * @return The NULL value if the patient is non-existent, or a string containing the 
+   * Orthanc ID of the patient. This string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupPatient(
+    OrthancPluginContext*  context,
+    const char*            patientID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = patientID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupPatient, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for a study.
+   *
+   * Look for a study stored in Orthanc, using its Study Instance UID tag (0x0020, 0x000d).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored studies).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param studyUID The Study Instance UID of interest.
+   * @return The NULL value if the study is non-existent, or a string containing the 
+   * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudy(
+    OrthancPluginContext*  context,
+    const char*            studyUID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = studyUID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupStudy, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for a study, using the accession number.
+   *
+   * Look for a study stored in Orthanc, using its Accession Number tag (0x0008, 0x0050).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored studies).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param accessionNumber The Accession Number of interest.
+   * @return The NULL value if the study is non-existent, or a string containing the 
+   * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudyWithAccessionNumber(
+    OrthancPluginContext*  context,
+    const char*            accessionNumber)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = accessionNumber;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupStudyWithAccessionNumber, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for a series.
+   *
+   * Look for a series stored in Orthanc, using its Series Instance UID tag (0x0020, 0x000e).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored series).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param seriesUID The Series Instance UID of interest.
+   * @return The NULL value if the series is non-existent, or a string containing the 
+   * Orthanc ID of the series. This string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupSeries(
+    OrthancPluginContext*  context,
+    const char*            seriesUID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = seriesUID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupSeries, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for an instance.
+   *
+   * Look for an instance stored in Orthanc, using its SOP Instance UID tag (0x0008, 0x0018).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored instances).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param sopInstanceUID The SOP Instance UID of interest.
+   * @return The NULL value if the instance is non-existent, or a string containing the 
+   * Orthanc ID of the instance. This string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupInstance(
+    OrthancPluginContext*  context,
+    const char*            sopInstanceUID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = sopInstanceUID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupInstance, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    uint16_t                 status;
+  } _OrthancPluginSendHttpStatusCode;
+
+  /**
+   * @brief Send a HTTP status code.
+   *
+   * This function answers to a REST request by sending a HTTP status
+   * code (such as "400 - Bad Request"). Note that:
+   * - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer().
+   * - Redirections (status 301) must use ::OrthancPluginRedirect().
+   * - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized().
+   * - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param status The HTTP status code to be sent.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendHttpStatusCode(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    uint16_t                 status)
+  {
+    _OrthancPluginSendHttpStatusCode params;
+    params.output = output;
+    params.status = status;
+    context->InvokeService(context, _OrthancPluginService_SendHttpStatusCode, &params);
+  }
+
+
+  /**
+   * @brief Signal that a REST request is not authorized.
+   *
+   * This function answers to a REST request by signaling that it is
+   * not authorized.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param realm The realm for the authorization process.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendUnauthorized(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              realm)
+  {
+    _OrthancPluginOutputPlusArgument params;
+    params.output = output;
+    params.argument = realm;
+    context->InvokeService(context, _OrthancPluginService_SendUnauthorized, &params);
+  }
+
+
+  /**
+   * @brief Signal that this URI does not support this HTTP method.
+   *
+   * This function answers to a REST request by signaling that the
+   * queried URI does not support this method.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param allowedMethods The allowed methods for this URI (e.g. "GET,POST" after a PUT or a POST request).
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendMethodNotAllowed(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              allowedMethods)
+  {
+    _OrthancPluginOutputPlusArgument params;
+    params.output = output;
+    params.argument = allowedMethods;
+    context->InvokeService(context, _OrthancPluginService_SendMethodNotAllowed, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              key;
+    const char*              value;
+  } _OrthancPluginSetHttpHeader;
+
+  /**
+   * @brief Set a cookie.
+   *
+   * This function sets a cookie in the HTTP client.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param cookie The cookie to be set.
+   * @param value The value of the cookie.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetCookie(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              cookie,
+    const char*              value)
+  {
+    _OrthancPluginSetHttpHeader params;
+    params.output = output;
+    params.key = cookie;
+    params.value = value;
+    context->InvokeService(context, _OrthancPluginService_SetCookie, &params);
+  }
+
+
+  /**
+   * @brief Set some HTTP header.
+   *
+   * This function sets a HTTP header in the HTTP answer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param key The HTTP header to be set.
+   * @param value The value of the HTTP header.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpHeader(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              key,
+    const char*              value)
+  {
+    _OrthancPluginSetHttpHeader params;
+    params.output = output;
+    params.key = key;
+    params.value = value;
+    context->InvokeService(context, _OrthancPluginService_SetHttpHeader, &params);
+  }
+
+
+  typedef struct
+  {
+    char**                      resultStringToFree;
+    const char**                resultString;
+    int64_t*                    resultInt64;
+    const char*                 key;
+    OrthancPluginDicomInstance* instance;
+  } _OrthancPluginAccessDicomInstance;
+
+
+  /**
+   * @brief Get the AET of a DICOM instance.
+   *
+   * This function returns the Application Entity Title (AET) of the
+   * DICOM modality from which a DICOM instance originates.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The AET if success, NULL if error.
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet(
+    OrthancPluginContext*        context,
+    OrthancPluginDicomInstance*  instance)
+  {
+    const char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultString = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceRemoteAet, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the size of a DICOM file.
+   *
+   * This function returns the number of bytes of the given DICOM instance.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The size of the file, -1 in case of error.
+   **/
+  ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize(
+    OrthancPluginContext*       context,
+    OrthancPluginDicomInstance* instance)
+  {
+    int64_t size;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultInt64 = &size;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceSize, &params))
+    {
+      /* Error */
+      return -1;
+    }
+    else
+    {
+      return size;
+    }
+  }
+
+
+  /**
+   * @brief Get the data of a DICOM file.
+   *
+   * This function returns a pointer to the content of the given DICOM instance.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The pointer to the DICOM data, NULL in case of error.
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceData(
+    OrthancPluginContext*        context,
+    OrthancPluginDicomInstance*  instance)
+  {
+    const char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultString = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceData, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the DICOM tag hierarchy as a JSON file.
+   *
+   * This function returns a pointer to a newly created string
+   * containing a JSON file. This JSON file encodes the tag hierarchy
+   * of the given DICOM instance.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The NULL value in case of error, or a string containing the JSON file.
+   * This string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson(
+    OrthancPluginContext*        context,
+    OrthancPluginDicomInstance*  instance)
+  {
+    char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultStringToFree = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceJson, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the DICOM tag hierarchy as a JSON file (with simplification).
+   *
+   * This function returns a pointer to a newly created string
+   * containing a JSON file. This JSON file encodes the tag hierarchy
+   * of the given DICOM instance. In contrast with
+   * ::OrthancPluginGetInstanceJson(), the returned JSON file is in
+   * its simplified version.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The NULL value in case of error, or a string containing the JSON file.
+   * This string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson(
+    OrthancPluginContext*        context,
+    OrthancPluginDicomInstance*  instance)
+  {
+    char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultStringToFree = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceSimplifiedJson, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Check whether a DICOM instance is associated with some metadata.
+   *
+   * This function checks whether the DICOM instance of interest is
+   * associated with some metadata. As of Orthanc 0.8.1, in the
+   * callbacks registered by
+   * ::OrthancPluginRegisterOnStoredInstanceCallback(), the only
+   * possibly available metadata are "ReceptionDate", "RemoteAET" and
+   * "IndexInSeries".
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @param metadata The metadata of interest.
+   * @return 1 if the metadata is present, 0 if it is absent, -1 in case of error.
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginHasInstanceMetadata(
+    OrthancPluginContext*        context,
+    OrthancPluginDicomInstance*  instance,
+    const char*                  metadata)
+  {
+    int64_t result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultInt64 = &result;
+    params.instance = instance;
+    params.key = metadata;
+
+    if (context->InvokeService(context, _OrthancPluginService_HasInstanceMetadata, &params))
+    {
+      /* Error */
+      return -1;
+    }
+    else
+    {
+      return (result != 0);
+    }
+  }
+
+
+  /**
+   * @brief Get the value of some metadata associated with a given DICOM instance.
+   *
+   * This functions returns the value of some metadata that is associated with the DICOM instance of interest.
+   * Before calling this function, the existence of the metadata must have been checked with
+   * ::OrthancPluginHasInstanceMetadata().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @param metadata The metadata of interest.
+   * @return The metadata value if success, NULL if error.
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata(
+    OrthancPluginContext*        context,
+    OrthancPluginDicomInstance*  instance,
+    const char*                  metadata)
+  {
+    const char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultString = &result;
+    params.instance = instance;
+    params.key = metadata;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceMetadata, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginStorageCreate  create;
+    OrthancPluginStorageRead    read;
+    OrthancPluginStorageRemove  remove;
+    OrthancPluginFree           free;
+  } _OrthancPluginRegisterStorageArea;
+
+  /**
+   * @brief Register a custom storage area.
+   *
+   * This function registers a custom storage area, to replace the
+   * built-in way Orthanc stores its files on the filesystem. This
+   * function must be called during the initialization of the plugin,
+   * i.e. inside the OrthancPluginInitialize() public function.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param create The callback function to store a file on the custom storage area.
+   * @param read The callback function to read a file from the custom storage area.
+   * @param remove The callback function to remove a file from the custom storage area.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea(
+    OrthancPluginContext*       context,
+    OrthancPluginStorageCreate  create,
+    OrthancPluginStorageRead    read,
+    OrthancPluginStorageRemove  remove)
+  {
+    _OrthancPluginRegisterStorageArea params;
+    params.create = create;
+    params.read = read;
+    params.remove = remove;
+
+#ifdef  __cplusplus
+    params.free = ::free;
+#else
+    params.free = free;
+#endif
+
+    context->InvokeService(context, _OrthancPluginService_RegisterStorageArea, &params);
+  }
+
+
+
+  /**
+   * @brief Return the path to the Orthanc executable.
+   *
+   * This function returns the path to the Orthanc executable.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the path. This string must be freed by
+   * OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancPath(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetOrthancPath, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Return the directory containing the Orthanc.
+   *
+   * This function returns the path to the directory containing the Orthanc executable.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the path. This string must be freed by
+   * OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancDirectory(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetOrthancDirectory, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Return the path to the configuration file.
+   *
+   * This function returns the path to the configuration file that was
+   * specified when starting Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the path. This string must be freed by
+   * OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfigurationPath(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetConfigurationPath, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginOnChangeCallback callback;
+  } _OrthancPluginOnChangeCallback;
+
+  /**
+   * @brief Register a callback to monitor changes.
+   *
+   * This function registers a callback function that is called
+   * whenever a change happens to some DICOM resource.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback function.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnChangeCallback(
+    OrthancPluginContext*          context,
+    OrthancPluginOnChangeCallback  callback)
+  {
+    _OrthancPluginOnChangeCallback params;
+    params.callback = callback;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterOnChangeCallback, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const char* plugin;
+    _OrthancPluginProperty property;
+    const char* value;
+  } _OrthancPluginSetPluginProperty;
+
+
+  /**
+   * @brief Set the URI where the plugin provides its Web interface.
+   *
+   * For plugins that come with a Web interface, this function
+   * declares the entry path where to find this interface. This
+   * information is notably used in the "Plugins" page of Orthanc
+   * Explorer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param uri The root URI for this plugin.
+   **/ 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri(
+    OrthancPluginContext*  context,
+    const char*            uri)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_RootUri;
+    params.value = uri;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
+   * @brief Set a description for this plugin.
+   *
+   * Set a description for this plugin. It is displayed in the
+   * "Plugins" page of Orthanc Explorer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param description The description.
+   **/ 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription(
+    OrthancPluginContext*  context,
+    const char*            description)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_Description;
+    params.value = description;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
+   * @brief Extend the JavaScript code of Orthanc Explorer.
+   *
+   * Add JavaScript code to customize the default behavior of Orthanc
+   * Explorer. This can for instance be used to add new buttons.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param javascript The custom JavaScript code.
+   **/ 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer(
+    OrthancPluginContext*  context,
+    const char*            javascript)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_OrthancExplorer;
+    params.value = javascript;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  typedef struct
+  {
+    char**       result;
+    int32_t      property;
+    const char*  value;
+  } _OrthancPluginGlobalProperty;
+
+
+  /**
+   * @brief Get the value of a global property.
+   *
+   * Get the value of a global property that is stored in the Orthanc database. Global
+   * properties whose index is below 1024 are reserved by Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param property The global property of interest.
+   * @param defaultValue The value to return, if the global property is unset.
+   * @return The value of the global property, or NULL in the case of an error. This
+   * string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetGlobalProperty(
+    OrthancPluginContext*  context,
+    int32_t                property,
+    const char*            defaultValue)
+  {
+    char* result;
+
+    _OrthancPluginGlobalProperty params;
+    params.result = &result;
+    params.property = property;
+    params.value = defaultValue;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetGlobalProperty, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Set the value of a global property.
+   *
+   * Set the value of a global property into the Orthanc
+   * database. Setting a global property can be used by plugins to
+   * save their internal parameters. Plugins are only allowed to set
+   * properties whose index are above or equal to 1024 (properties
+   * below 1024 are read-only and reserved by Orthanc).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param property The global property of interest.
+   * @param value The value to be set in the global property.
+   * @return 0 if success, -1 in case of error.
+   **/
+  ORTHANC_PLUGIN_INLINE int32_t OrthancPluginSetGlobalProperty(
+    OrthancPluginContext*  context,
+    int32_t                property,
+    const char*            value)
+  {
+    _OrthancPluginGlobalProperty params;
+    params.result = NULL;
+    params.property = property;
+    params.value = value;
+
+    if (context->InvokeService(context, _OrthancPluginService_SetGlobalProperty, &params))
+    {
+      /* Error */
+      return -1;
+    }
+    else
+    {
+      return 0;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    int32_t   *resultInt32;
+    uint32_t  *resultUint32;
+    int64_t   *resultInt64;
+    uint64_t  *resultUint64;
+  } _OrthancPluginReturnSingleValue;
+
+  /**
+   * @brief Get the number of command-line arguments.
+   *
+   * Retrieve the number of command-line arguments that were used to launch Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return The number of arguments.
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetCommandLineArgumentsCount(
+    OrthancPluginContext*  context)
+  {
+    uint32_t count = 0;
+
+    _OrthancPluginReturnSingleValue params;
+    memset(&params, 0, sizeof(params));
+    params.resultUint32 = &count;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgumentsCount, &params))
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+
+  /**
+   * @brief Get the value of a command-line argument.
+   *
+   * Get the value of one of the command-line arguments that were used
+   * to launch Orthanc. The number of available arguments can be
+   * retrieved by OrthancPluginGetCommandLineArgumentsCount().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param argument The index of the argument.
+   * @return The value of the argument, or NULL in the case of an error. This
+   * string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetCommandLineArgument(
+    OrthancPluginContext*  context,
+    uint32_t               argument)
+  {
+    char* result;
+
+    _OrthancPluginGlobalProperty params;
+    params.result = &result;
+    params.property = (int32_t) argument;
+    params.value = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgument, &params))
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+#ifdef  __cplusplus
+}
+#endif
+
+
+/** @} */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Include/OrthancCppDatabasePlugin.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,1532 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "OrthancCDatabasePlugin.h"
+
+#include <stdexcept>
+#include <list>
+#include <string>
+
+namespace OrthancPlugins
+{
+  // This class mimics "boost::noncopyable"
+  class NonCopyable
+  {
+  private:
+    NonCopyable(const NonCopyable&);
+
+    NonCopyable& operator= (const NonCopyable&);
+
+  protected:
+    NonCopyable()
+    {
+    }
+
+    ~NonCopyable()
+    {
+    }
+  };
+
+
+
+  class DatabaseBackendOutput : public NonCopyable
+  {
+    friend class DatabaseBackendAdapter;
+
+  private:
+    enum AllowedAnswers
+    {
+      AllowedAnswers_All,
+      AllowedAnswers_None,
+      AllowedAnswers_Attachment,
+      AllowedAnswers_Change,
+      AllowedAnswers_DicomTag,
+      AllowedAnswers_ExportedResource
+    };
+
+    OrthancPluginContext*         context_;
+    OrthancPluginDatabaseContext* database_;
+    AllowedAnswers                allowedAnswers_;
+
+    void SetAllowedAnswers(AllowedAnswers allowed)
+    {
+      allowedAnswers_ = allowed;
+    }
+
+  public:
+    DatabaseBackendOutput(OrthancPluginContext*         context,
+                          OrthancPluginDatabaseContext* database) :
+      context_(context),
+      database_(database),
+      allowedAnswers_(AllowedAnswers_All /* for unit tests */)
+    {
+    }
+
+    void LogError(const std::string& message)
+    {
+      OrthancPluginLogError(context_, message.c_str());
+    }
+
+    void LogWarning(const std::string& message)
+    {
+      OrthancPluginLogWarning(context_, message.c_str());
+    }
+
+    void LogInfo(const std::string& message)
+    {
+      OrthancPluginLogInfo(context_, message.c_str());
+    }
+
+    void SignalDeletedAttachment(const std::string& uuid,
+                                 int32_t            contentType,
+                                 uint64_t           uncompressedSize,
+                                 const std::string& uncompressedHash,
+                                 int32_t            compressionType,
+                                 uint64_t           compressedSize,
+                                 const std::string& compressedHash)
+    {
+      OrthancPluginAttachment attachment;
+      attachment.uuid = uuid.c_str();
+      attachment.contentType = contentType;
+      attachment.uncompressedSize = uncompressedSize;
+      attachment.uncompressedHash = uncompressedHash.c_str();
+      attachment.compressionType = compressionType;
+      attachment.compressedSize = compressedSize;
+      attachment.compressedHash = compressedHash.c_str();
+
+      OrthancPluginDatabaseSignalDeletedAttachment(context_, database_, &attachment);
+    }
+
+    void SignalDeletedResource(const std::string& publicId,
+                               OrthancPluginResourceType resourceType)
+    {
+      OrthancPluginDatabaseSignalDeletedResource(context_, database_, publicId.c_str(), resourceType);
+    }
+
+    void SignalRemainingAncestor(const std::string& ancestorId,
+                                 OrthancPluginResourceType ancestorType)
+    {
+      OrthancPluginDatabaseSignalRemainingAncestor(context_, database_, ancestorId.c_str(), ancestorType);
+    }
+
+    void AnswerAttachment(const std::string& uuid,
+                          int32_t            contentType,
+                          uint64_t           uncompressedSize,
+                          const std::string& uncompressedHash,
+                          int32_t            compressionType,
+                          uint64_t           compressedSize,
+                          const std::string& compressedHash)
+    {
+      if (allowedAnswers_ != AllowedAnswers_All &&
+          allowedAnswers_ != AllowedAnswers_Attachment)
+      {
+        throw std::runtime_error("Cannot answer with an attachment in the current state");
+      }
+
+      OrthancPluginAttachment attachment;
+      attachment.uuid = uuid.c_str();
+      attachment.contentType = contentType;
+      attachment.uncompressedSize = uncompressedSize;
+      attachment.uncompressedHash = uncompressedHash.c_str();
+      attachment.compressionType = compressionType;
+      attachment.compressedSize = compressedSize;
+      attachment.compressedHash = compressedHash.c_str();
+
+      OrthancPluginDatabaseAnswerAttachment(context_, database_, &attachment);
+    }
+
+    void AnswerChange(int64_t                    seq,
+                      int32_t                    changeType,
+                      OrthancPluginResourceType  resourceType,
+                      const std::string&         publicId,
+                      const std::string&         date)
+    {
+      if (allowedAnswers_ != AllowedAnswers_All &&
+          allowedAnswers_ != AllowedAnswers_Change)
+      {
+        throw std::runtime_error("Cannot answer with a change in the current state");
+      }
+
+      OrthancPluginChange change;
+      change.seq = seq;
+      change.changeType = changeType;
+      change.resourceType = resourceType;
+      change.publicId = publicId.c_str();
+      change.date = date.c_str();
+
+      OrthancPluginDatabaseAnswerChange(context_, database_, &change);
+    }
+
+    void AnswerDicomTag(uint16_t group,
+                        uint16_t element,
+                        const std::string& value)
+    {
+      if (allowedAnswers_ != AllowedAnswers_All &&
+          allowedAnswers_ != AllowedAnswers_DicomTag)
+      {
+        throw std::runtime_error("Cannot answer with a DICOM tag in the current state");
+      }
+
+      OrthancPluginDicomTag tag;
+      tag.group = group;
+      tag.element = element;
+      tag.value = value.c_str();
+
+      OrthancPluginDatabaseAnswerDicomTag(context_, database_, &tag);
+    }
+
+    void AnswerExportedResource(int64_t                    seq,
+                                OrthancPluginResourceType  resourceType,
+                                const std::string&         publicId,
+                                const std::string&         modality,
+                                const std::string&         date,
+                                const std::string&         patientId,
+                                const std::string&         studyInstanceUid,
+                                const std::string&         seriesInstanceUid,
+                                const std::string&         sopInstanceUid)
+    {
+      if (allowedAnswers_ != AllowedAnswers_All &&
+          allowedAnswers_ != AllowedAnswers_ExportedResource)
+      {
+        throw std::runtime_error("Cannot answer with an exported resource in the current state");
+      }
+
+      OrthancPluginExportedResource exported;
+      exported.seq = seq;
+      exported.resourceType = resourceType;
+      exported.publicId = publicId.c_str();
+      exported.modality = modality.c_str();
+      exported.date = date.c_str();
+      exported.patientId = patientId.c_str();
+      exported.studyInstanceUid = studyInstanceUid.c_str();
+      exported.seriesInstanceUid = seriesInstanceUid.c_str();
+      exported.sopInstanceUid = sopInstanceUid.c_str();
+
+      OrthancPluginDatabaseAnswerExportedResource(context_, database_, &exported);
+    }
+  };
+
+
+
+  class IDatabaseBackend : public NonCopyable
+  {
+    friend class DatabaseBackendAdapter;
+
+  private:
+    DatabaseBackendOutput*  output_;
+
+    void Finalize()
+    {
+      if (output_ != NULL)
+      {
+        delete output_;
+        output_ = NULL;
+      }
+    }
+
+  protected:
+    DatabaseBackendOutput& GetOutput()
+    {
+      return *output_;
+    }
+
+  public:
+    IDatabaseBackend() : output_(NULL)
+    {
+    }
+
+    virtual ~IDatabaseBackend()
+    {
+      Finalize();
+    }
+
+    // This takes the ownership
+    void RegisterOutput(DatabaseBackendOutput* output)
+    {
+      Finalize();
+      output_ = output;
+    }
+
+    virtual void Open() = 0;
+
+    virtual void Close() = 0;
+
+    virtual void AddAttachment(int64_t id,
+                               const OrthancPluginAttachment& attachment) = 0;
+
+    virtual void AttachChild(int64_t parent,
+                             int64_t child) = 0;
+
+    virtual void ClearChanges() = 0;
+
+    virtual void ClearExportedResources() = 0;
+
+    virtual int64_t CreateResource(const char* publicId,
+                                   OrthancPluginResourceType type) = 0;
+
+    virtual void DeleteAttachment(int64_t id,
+                                  int32_t attachment) = 0;
+
+    virtual void DeleteMetadata(int64_t id,
+                                int32_t metadataType) = 0;
+
+    virtual void DeleteResource(int64_t id) = 0;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 OrthancPluginResourceType resourceType) = 0;
+
+    /* Use GetOutput().AnswerChange() */
+    virtual void GetChanges(bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults) = 0;
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target /*out*/,
+                                       int64_t id) = 0;
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target /*out*/,
+                                     int64_t id) = 0;
+
+    /* Use GetOutput().AnswerExportedResource() */
+    virtual void GetExportedResources(bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults) = 0;
+
+    /* Use GetOutput().AnswerChange() */
+    virtual void GetLastChange() = 0;
+
+    /* Use GetOutput().AnswerExportedResource() */
+    virtual void GetLastExportedResource() = 0;
+
+    /* Use GetOutput().AnswerDicomTag() */
+    virtual void GetMainDicomTags(int64_t id) = 0;
+
+    virtual std::string GetPublicId(int64_t resourceId) = 0;
+
+    virtual uint64_t GetResourceCount(OrthancPluginResourceType resourceType) = 0;
+
+    virtual OrthancPluginResourceType GetResourceType(int64_t resourceId) = 0;
+
+    virtual uint64_t GetTotalCompressedSize() = 0;
+    
+    virtual uint64_t GetTotalUncompressedSize() = 0;
+
+    virtual bool IsExistingResource(int64_t internalId) = 0;
+
+    virtual bool IsProtectedPatient(int64_t internalId) = 0;
+
+    virtual void ListAvailableMetadata(std::list<int32_t>& target /*out*/,
+                                       int64_t id) = 0;
+
+    virtual void ListAvailableAttachments(std::list<int32_t>& target /*out*/,
+                                          int64_t id) = 0;
+
+    virtual void LogChange(const OrthancPluginChange& change) = 0;
+
+    virtual void LogExportedResource(const OrthancPluginExportedResource& resource) = 0;
+    
+    /* Use GetOutput().AnswerAttachment() */
+    virtual bool LookupAttachment(int64_t id,
+                                  int32_t contentType) = 0;
+
+    virtual bool LookupGlobalProperty(std::string& target /*out*/,
+                                      int32_t property) = 0;
+
+    /**
+     * "Identifiers" are necessarily one of the following tags:
+     * PatientID (0x0010, 0x0020), StudyInstanceUID (0x0020, 0x000d),
+     * SeriesInstanceUID (0x0020, 0x000e), SOPInstanceUID (0x0008,
+     * 0x0018) or AccessionNumber (0x0008, 0x0050).
+     **/
+    virtual void LookupIdentifier(std::list<int64_t>& target /*out*/,
+                                  uint16_t group,
+                                  uint16_t element,
+                                  const char* value) = 0;
+
+    virtual void LookupIdentifier(std::list<int64_t>& target /*out*/,
+                                  const char* value) = 0;
+
+    virtual bool LookupMetadata(std::string& target /*out*/,
+                                int64_t id,
+                                int32_t metadataType) = 0;
+
+    virtual bool LookupParent(int64_t& parentId /*out*/,
+                              int64_t resourceId) = 0;
+
+    virtual bool LookupResource(int64_t& id /*out*/,
+                                OrthancPluginResourceType& type /*out*/,
+                                const char* publicId) = 0;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/) = 0;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId /*out*/,
+                                        int64_t patientIdToAvoid) = 0;
+
+    virtual void SetGlobalProperty(int32_t property,
+                                   const char* value) = 0;
+
+    virtual void SetMainDicomTag(int64_t id,
+                                 uint16_t group,
+                                 uint16_t element,
+                                 const char* value) = 0;
+
+    virtual void SetIdentifierTag(int64_t id,
+                                  uint16_t group,
+                                  uint16_t element,
+                                  const char* value) = 0;
+
+    virtual void SetMetadata(int64_t id,
+                             int32_t metadataType,
+                             const char* value) = 0;
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected) = 0;
+
+    virtual void StartTransaction() = 0;
+
+    virtual void RollbackTransaction() = 0;
+
+    virtual void CommitTransaction() = 0;
+  };
+
+
+
+  class DatabaseBackendAdapter
+  {
+  private:
+    // This class cannot be instantiated
+    DatabaseBackendAdapter()
+    {
+    }
+
+    static void LogError(IDatabaseBackend* backend,
+                         const std::runtime_error& e)
+    {
+      backend->GetOutput().LogError("Exception in database back-end: " + std::string(e.what()));
+    }
+
+
+    static int32_t  AddAttachment(void* payload,
+                                  int64_t id,
+                                  const OrthancPluginAttachment* attachment)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->AddAttachment(id, *attachment);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+                             
+    static int32_t  AttachChild(void* payload,
+                                int64_t parent,
+                                int64_t child)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->AttachChild(parent, child);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+          
+                   
+    static int32_t  ClearChanges(void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->ClearChanges();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+                             
+
+    static int32_t  ClearExportedResources(void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->ClearExportedResources();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  CreateResource(int64_t* id, 
+                                   void* payload,
+                                   const char* publicId,
+                                   OrthancPluginResourceType resourceType)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        *id = backend->CreateResource(publicId, resourceType);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+          
+         
+    static int32_t  DeleteAttachment(void* payload,
+                                     int64_t id,
+                                     int32_t contentType)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->DeleteAttachment(id, contentType);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+   
+
+    static int32_t  DeleteMetadata(void* payload,
+                                   int64_t id,
+                                   int32_t metadataType)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->DeleteMetadata(id, metadataType);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+   
+
+    static int32_t  DeleteResource(void* payload,
+                                   int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->DeleteResource(id);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  GetAllPublicIds(OrthancPluginDatabaseContext* context,
+                                    void* payload,
+                                    OrthancPluginResourceType resourceType)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::list<std::string> ids;
+        backend->GetAllPublicIds(ids, resourceType);
+
+        for (std::list<std::string>::const_iterator
+               it = ids.begin(); it != ids.end(); ++it)
+        {
+          OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
+                                            backend->GetOutput().database_,
+                                            it->c_str());
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  GetChanges(OrthancPluginDatabaseContext* context,
+                               void* payload,
+                               int64_t since,
+                               uint32_t maxResult)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_Change);
+
+      try
+      {
+        bool done;
+        backend->GetChanges(done, since, maxResult);
+        
+        if (done)
+        {
+          OrthancPluginDatabaseAnswerChangesDone(backend->GetOutput().context_,
+                                                 backend->GetOutput().database_);
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  GetChildrenInternalId(OrthancPluginDatabaseContext* context,
+                                          void* payload,
+                                          int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::list<int64_t> target;
+        backend->GetChildrenInternalId(target, id);
+
+        for (std::list<int64_t>::const_iterator
+               it = target.begin(); it != target.end(); ++it)
+        {
+          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
+                                           backend->GetOutput().database_, *it);
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+          
+         
+    static int32_t  GetChildrenPublicId(OrthancPluginDatabaseContext* context,
+                                        void* payload,
+                                        int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::list<std::string> ids;
+        backend->GetChildrenPublicId(ids, id);
+
+        for (std::list<std::string>::const_iterator
+               it = ids.begin(); it != ids.end(); ++it)
+        {
+          OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
+                                            backend->GetOutput().database_,
+                                            it->c_str());
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  GetExportedResources(OrthancPluginDatabaseContext* context,
+                                         void* payload,
+                                         int64_t  since,
+                                         uint32_t  maxResult)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_ExportedResource);
+
+      try
+      {
+        bool done;
+        backend->GetExportedResources(done, since, maxResult);
+
+        if (done)
+        {
+          OrthancPluginDatabaseAnswerExportedResourcesDone(backend->GetOutput().context_,
+                                                           backend->GetOutput().database_);
+        }
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+          
+         
+    static int32_t  GetLastChange(OrthancPluginDatabaseContext* context,
+                                  void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_Change);
+
+      try
+      {
+        backend->GetLastChange();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  GetLastExportedResource(OrthancPluginDatabaseContext* context,
+                                            void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_ExportedResource);
+
+      try
+      {
+        backend->GetLastExportedResource();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+    
+               
+    static int32_t  GetMainDicomTags(OrthancPluginDatabaseContext* context,
+                                     void* payload,
+                                     int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_DicomTag);
+
+      try
+      {
+        backend->GetMainDicomTags(id);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+          
+         
+    static int32_t  GetPublicId(OrthancPluginDatabaseContext* context,
+                                void* payload,
+                                int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::string s = backend->GetPublicId(id);
+        OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
+                                          backend->GetOutput().database_,
+                                          s.c_str());
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  GetResourceCount(uint64_t* target,
+                                     void* payload,
+                                     OrthancPluginResourceType  resourceType)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        *target = backend->GetResourceCount(resourceType);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+                   
+
+    static int32_t  GetResourceType(OrthancPluginResourceType* resourceType,
+                                    void* payload,
+                                    int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        *resourceType = backend->GetResourceType(id);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  GetTotalCompressedSize(uint64_t* target,
+                                           void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        *target = backend->GetTotalCompressedSize();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+          
+         
+    static int32_t  GetTotalUncompressedSize(uint64_t* target,
+                                             void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        *target = backend->GetTotalUncompressedSize();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+                   
+
+    static int32_t  IsExistingResource(int32_t* existing,
+                                       void* payload,
+                                       int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        *existing = backend->IsExistingResource(id);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  IsProtectedPatient(int32_t* isProtected,
+                                       void* payload,
+                                       int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        *isProtected = backend->IsProtectedPatient(id);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  ListAvailableMetadata(OrthancPluginDatabaseContext* context,
+                                          void* payload,
+                                          int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::list<int32_t> target;
+        backend->ListAvailableMetadata(target, id);
+
+        for (std::list<int32_t>::const_iterator
+               it = target.begin(); it != target.end(); ++it)
+        {
+          OrthancPluginDatabaseAnswerInt32(backend->GetOutput().context_,
+                                           backend->GetOutput().database_,
+                                           *it);
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+          
+         
+    static int32_t  ListAvailableAttachments(OrthancPluginDatabaseContext* context,
+                                             void* payload,
+                                             int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::list<int32_t> target;
+        backend->ListAvailableAttachments(target, id);
+
+        for (std::list<int32_t>::const_iterator
+               it = target.begin(); it != target.end(); ++it)
+        {
+          OrthancPluginDatabaseAnswerInt32(backend->GetOutput().context_,
+                                           backend->GetOutput().database_,
+                                           *it);
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  LogChange(void* payload,
+                              const OrthancPluginChange* change)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->LogChange(*change);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+          
+         
+    static int32_t  LogExportedResource(void* payload,
+                                        const OrthancPluginExportedResource* exported)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->LogExportedResource(*exported);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+          
+         
+    static int32_t  LookupAttachment(OrthancPluginDatabaseContext* context,
+                                     void* payload,
+                                     int64_t id,
+                                     int32_t contentType)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_Attachment);
+
+      try
+      {
+        backend->LookupAttachment(id, contentType);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  LookupGlobalProperty(OrthancPluginDatabaseContext* context,
+                                         void* payload,
+                                         int32_t property)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::string s;
+        if (backend->LookupGlobalProperty(s, property))
+        {
+          OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
+                                            backend->GetOutput().database_,
+                                            s.c_str());
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  LookupIdentifier(OrthancPluginDatabaseContext* context,
+                                     void* payload,
+                                     const OrthancPluginDicomTag* tag)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::list<int64_t> target;
+        backend->LookupIdentifier(target, tag->group, tag->element, tag->value);
+
+        for (std::list<int64_t>::const_iterator
+               it = target.begin(); it != target.end(); ++it)
+        {
+          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
+                                           backend->GetOutput().database_, *it);
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  LookupIdentifier2(OrthancPluginDatabaseContext* context,
+                                      void* payload,
+                                      const char* value)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::list<int64_t> target;
+        backend->LookupIdentifier(target, value);
+
+        for (std::list<int64_t>::const_iterator
+               it = target.begin(); it != target.end(); ++it)
+        {
+          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
+                                           backend->GetOutput().database_, *it);
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  LookupMetadata(OrthancPluginDatabaseContext* context,
+                                   void* payload,
+                                   int64_t id,
+                                   int32_t metadata)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        std::string s;
+        if (backend->LookupMetadata(s, id, metadata))
+        {
+          OrthancPluginDatabaseAnswerString(backend->GetOutput().context_,
+                                            backend->GetOutput().database_, s.c_str());
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  LookupParent(OrthancPluginDatabaseContext* context,
+                                 void* payload,
+                                 int64_t id)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        int64_t parent;
+        if (backend->LookupParent(parent, id))
+        {
+          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
+                                           backend->GetOutput().database_, parent);
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  LookupResource(OrthancPluginDatabaseContext* context,
+                                   void* payload,
+                                   const char* publicId)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        int64_t id;
+        OrthancPluginResourceType type;
+        if (backend->LookupResource(id, type, publicId))
+        {
+          OrthancPluginDatabaseAnswerResource(backend->GetOutput().context_,
+                                              backend->GetOutput().database_, 
+                                              id, type);
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  SelectPatientToRecycle(OrthancPluginDatabaseContext* context,
+                                           void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        int64_t id;
+        if (backend->SelectPatientToRecycle(id))
+        {
+          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
+                                           backend->GetOutput().database_, id);
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  SelectPatientToRecycle2(OrthancPluginDatabaseContext* context,
+                                            void* payload,
+                                            int64_t patientIdToAvoid)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        int64_t id;
+        if (backend->SelectPatientToRecycle(id, patientIdToAvoid))
+        {
+          OrthancPluginDatabaseAnswerInt64(backend->GetOutput().context_,
+                                           backend->GetOutput().database_, id);
+        }
+
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  SetGlobalProperty(void* payload,
+                                      int32_t property,
+                                      const char* value)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->SetGlobalProperty(property, value);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  SetMainDicomTag(void* payload,
+                                    int64_t id,
+                                    const OrthancPluginDicomTag* tag)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->SetMainDicomTag(id, tag->group, tag->element, tag->value);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  SetIdentifierTag(void* payload,
+                                    int64_t id,
+                                    const OrthancPluginDicomTag* tag)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->SetIdentifierTag(id, tag->group, tag->element, tag->value);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  SetMetadata(void* payload,
+                                int64_t id,
+                                int32_t metadata,
+                                const char* value)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->SetMetadata(id, metadata, value);
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t  SetProtectedPatient(void* payload,
+                                        int64_t id,
+                                        int32_t isProtected)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->SetProtectedPatient(id, (isProtected != 0));
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t StartTransaction(void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->StartTransaction();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t RollbackTransaction(void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->RollbackTransaction();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t CommitTransaction(void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->CommitTransaction();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t Open(void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->Open();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+
+    static int32_t Close(void* payload)
+    {
+      IDatabaseBackend* backend = reinterpret_cast<IDatabaseBackend*>(payload);
+      backend->GetOutput().SetAllowedAnswers(DatabaseBackendOutput::AllowedAnswers_None);
+
+      try
+      {
+        backend->Close();
+        return 0;
+      }
+      catch (std::runtime_error& e)
+      {
+        LogError(backend, e);
+        return -1;
+      }
+    }
+
+    
+  public:
+    static void Register(OrthancPluginContext* context,
+                         IDatabaseBackend& backend)
+    {
+      OrthancPluginDatabaseBackend  params;
+      memset(&params, 0, sizeof(params));
+
+      params.addAttachment = AddAttachment;
+      params.attachChild = AttachChild;
+      params.clearChanges = ClearChanges;
+      params.clearExportedResources = ClearExportedResources;
+      params.createResource = CreateResource;
+      params.deleteAttachment = DeleteAttachment;
+      params.deleteMetadata = DeleteMetadata;
+      params.deleteResource = DeleteResource;
+      params.getAllPublicIds = GetAllPublicIds;
+      params.getChanges = GetChanges;
+      params.getChildrenInternalId = GetChildrenInternalId;
+      params.getChildrenPublicId = GetChildrenPublicId;
+      params.getExportedResources = GetExportedResources;
+      params.getLastChange = GetLastChange;
+      params.getLastExportedResource = GetLastExportedResource;
+      params.getMainDicomTags = GetMainDicomTags;
+      params.getPublicId = GetPublicId;
+      params.getResourceCount = GetResourceCount;
+      params.getResourceType = GetResourceType;
+      params.getTotalCompressedSize = GetTotalCompressedSize;
+      params.getTotalUncompressedSize = GetTotalUncompressedSize;
+      params.isExistingResource = IsExistingResource;
+      params.isProtectedPatient = IsProtectedPatient;
+      params.listAvailableMetadata = ListAvailableMetadata;
+      params.listAvailableAttachments = ListAvailableAttachments;
+      params.logChange = LogChange;
+      params.logExportedResource = LogExportedResource;
+      params.lookupAttachment = LookupAttachment;
+      params.lookupGlobalProperty = LookupGlobalProperty;
+      params.lookupIdentifier = LookupIdentifier;
+      params.lookupIdentifier2 = LookupIdentifier2;
+      params.lookupMetadata = LookupMetadata;
+      params.lookupParent = LookupParent;
+      params.lookupResource = LookupResource;
+      params.selectPatientToRecycle = SelectPatientToRecycle;
+      params.selectPatientToRecycle2 = SelectPatientToRecycle2;
+      params.setGlobalProperty = SetGlobalProperty;
+      params.setMainDicomTag = SetMainDicomTag;
+      params.setIdentifierTag = SetIdentifierTag;
+      params.setMetadata = SetMetadata;
+      params.setProtectedPatient = SetProtectedPatient;
+      params.startTransaction = StartTransaction;
+      params.rollbackTransaction = RollbackTransaction;
+      params.commitTransaction = CommitTransaction;
+      params.open = Open;
+      params.close = Close;
+
+      OrthancPluginDatabaseContext* database = OrthancPluginRegisterDatabaseBackend(context, &params, &backend);
+      if (!context)
+      {
+        throw std::runtime_error("Unable to register the database backend");
+      }
+
+      backend.RegisterOutput(new DatabaseBackendOutput(context, database));
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Basic/CMakeLists.txt	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(Basic)
+
+if (${CMAKE_COMPILER_IS_GNUCXX})
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic -Werror")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Werror")
+endif()
+
+include_directories(${CMAKE_SOURCE_DIR}/../../Include/)
+add_library(PluginTest SHARED Plugin.c)
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+  # Linking with "pthread" is necessary, otherwise the software crashes
+  # http://sourceware.org/bugzilla/show_bug.cgi?id=10652#c17
+  target_link_libraries(PluginTest pthread dl)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/Basic/Plugin.c	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,440 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+#include <OrthancCPlugin.h>
+
+#include <string.h>
+#include <stdio.h>
+
+static OrthancPluginContext* context = NULL;
+
+
+ORTHANC_PLUGINS_API int32_t Callback1(OrthancPluginRestOutput* output,
+                                      const char* url,
+                                      const OrthancPluginHttpRequest* request)
+{
+  char buffer[1024];
+  uint32_t i;
+
+  sprintf(buffer, "Callback on URL [%s] with body [%s]\n", url, request->body);
+  OrthancPluginLogWarning(context, buffer);
+
+  OrthancPluginSetCookie(context, output, "hello", "world");
+  OrthancPluginAnswerBuffer(context, output, buffer, strlen(buffer), "text/plain");
+
+  OrthancPluginLogWarning(context, "");    
+
+  for (i = 0; i < request->groupsCount; i++)
+  {
+    sprintf(buffer, "  REGEX GROUP %d = [%s]", i, request->groups[i]);
+    OrthancPluginLogWarning(context, buffer);    
+  }
+
+  OrthancPluginLogWarning(context, "");    
+
+  for (i = 0; i < request->getCount; i++)
+  {
+    sprintf(buffer, "  GET [%s] = [%s]", request->getKeys[i], request->getValues[i]);
+    OrthancPluginLogWarning(context, buffer);    
+  }
+
+  OrthancPluginLogWarning(context, "");    
+
+  for (i = 0; i < request->headersCount; i++)
+  {
+    sprintf(buffer, "  HEADERS [%s] = [%s]", request->headersKeys[i], request->headersValues[i]);
+    OrthancPluginLogWarning(context, buffer);    
+  }
+
+  OrthancPluginLogWarning(context, "");
+
+  return 1;
+}
+
+
+ORTHANC_PLUGINS_API int32_t Callback2(OrthancPluginRestOutput* output,
+                                      const char* url,
+                                      const OrthancPluginHttpRequest* request)
+{
+  /* Answer with a sample 16bpp image. */
+
+  uint16_t buffer[256 * 256];
+  uint32_t x, y, value;
+
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    value = 0;
+    for (y = 0; y < 256; y++)
+    {
+      for (x = 0; x < 256; x++, value++)
+      {
+        buffer[value] = value;
+      }
+    }
+
+    OrthancPluginCompressAndAnswerPngImage(context, output, OrthancPluginPixelFormat_Grayscale16,
+                                           256, 256, sizeof(uint16_t) * 256, buffer);
+  }
+
+  return 0;
+}
+
+
+ORTHANC_PLUGINS_API int32_t Callback3(OrthancPluginRestOutput* output,
+                                      const char* url,
+                                      const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    OrthancPluginMemoryBuffer dicom;
+    if (!OrthancPluginGetDicomForInstance(context, &dicom, request->groups[0]))
+    {
+      /* No error, forward the DICOM file */
+      OrthancPluginAnswerBuffer(context, output, dicom.data, dicom.size, "application/dicom");
+
+      /* Free memory */
+      OrthancPluginFreeMemoryBuffer(context, &dicom);
+    }
+  }
+
+  return 0;
+}
+
+
+ORTHANC_PLUGINS_API int32_t Callback4(OrthancPluginRestOutput* output,
+                                      const char* url,
+                                      const OrthancPluginHttpRequest* request)
+{
+  /* Answer with a sample 8bpp image. */
+
+  uint8_t  buffer[256 * 256];
+  uint32_t x, y, value;
+
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    value = 0;
+    for (y = 0; y < 256; y++)
+    {
+      for (x = 0; x < 256; x++, value++)
+      {
+        buffer[value] = x;
+      }
+    }
+
+    OrthancPluginCompressAndAnswerPngImage(context, output, OrthancPluginPixelFormat_Grayscale8,
+                                           256, 256, 256, buffer);
+  }
+
+  return 0;
+}
+
+
+ORTHANC_PLUGINS_API int32_t Callback5(OrthancPluginRestOutput* output,
+                                      const char* url,
+                                      const OrthancPluginHttpRequest* request)
+{
+  /**
+   * Demonstration the difference between the
+   * "OrthancPluginRestApiXXX()" and the
+   * "OrthancPluginRestApiXXXAfterPlugins()" mechanisms to forward
+   * REST calls.
+   *
+   * # curl http://localhost:8042/forward/built-in/system
+   * # curl http://localhost:8042/forward/plugins/system
+   * # curl http://localhost:8042/forward/built-in/plugin/image
+   *   => FAILURE (because the "/plugin/image" URI is implemented by this plugin)
+   * # curl http://localhost:8042/forward/plugins/plugin/image  => SUCCESS
+   **/
+
+  OrthancPluginMemoryBuffer tmp;
+  int isBuiltIn, error;
+
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+    return 0;
+  }
+
+  isBuiltIn = strcmp("plugins", request->groups[0]);
+ 
+  if (isBuiltIn)
+  {
+    error = OrthancPluginRestApiGet(context, &tmp, request->groups[1]);
+  }
+  else
+  {
+    printf("ICI1\n");
+    error = OrthancPluginRestApiGetAfterPlugins(context, &tmp, request->groups[1]);
+    printf("ICI2\n");
+  }
+
+  if (error)
+  {
+    return -1;
+  }
+  else
+  {
+    OrthancPluginAnswerBuffer(context, output, tmp.data, tmp.size, "application/octet-stream");
+    OrthancPluginFreeMemoryBuffer(context, &tmp);
+    return 0;
+  }
+}
+
+
+ORTHANC_PLUGINS_API int32_t CallbackCreateDicom(OrthancPluginRestOutput* output,
+                                                const char* url,
+                                                const OrthancPluginHttpRequest* request)
+{
+  const char* pathLocator = "\"Path\" : \"";
+  char info[1024];
+  char *id, *eos;
+  OrthancPluginMemoryBuffer tmp;
+
+  if (request->method != OrthancPluginHttpMethod_Post)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "POST");
+  }
+  else
+  {
+    /* Make POST request to create a new DICOM instance */
+    sprintf(info, "{\"PatientName\":\"Test\"}");
+    OrthancPluginRestApiPost(context, &tmp, "/tools/create-dicom", info, strlen(info));
+
+    /**
+     * Recover the ID of the created instance is constructed by a
+     * quick-and-dirty parsing of a JSON string.
+     **/
+    id = strstr((char*) tmp.data, pathLocator) + strlen(pathLocator);
+    eos = strchr(id, '\"');
+    eos[0] = '\0';
+
+    /* Delete the newly created DICOM instance. */
+    OrthancPluginRestApiDelete(context, id);
+    OrthancPluginFreeMemoryBuffer(context, &tmp);
+
+    /* Set some cookie */
+    OrthancPluginSetCookie(context, output, "hello", "world");
+
+    /* Set some HTTP header */
+    OrthancPluginSetHttpHeader(context, output, "Cache-Control", "max-age=0, no-cache");
+    
+    OrthancPluginAnswerBuffer(context, output, "OK\n", 3, "text/plain");
+  }
+
+  return 0;
+}
+
+
+ORTHANC_PLUGINS_API int32_t OnStoredCallback(OrthancPluginDicomInstance* instance,
+                                             const char* instanceId)
+{
+  char buffer[256];
+  FILE* fp;
+  char* json;
+  static int first = 1;
+
+  sprintf(buffer, "Just received a DICOM instance of size %d and ID %s from AET %s", 
+          (int) OrthancPluginGetInstanceSize(context, instance), instanceId, 
+          OrthancPluginGetInstanceRemoteAet(context, instance));
+
+  OrthancPluginLogWarning(context, buffer);  
+
+  fp = fopen("PluginReceivedInstance.dcm", "wb");
+  fwrite(OrthancPluginGetInstanceData(context, instance),
+         OrthancPluginGetInstanceSize(context, instance), 1, fp);
+  fclose(fp);
+
+  json = OrthancPluginGetInstanceSimplifiedJson(context, instance);
+  if (first)
+  {
+    /* Only print the first DICOM instance */
+    printf("[%s]\n", json);
+    first = 0;
+  }
+  OrthancPluginFreeString(context, json);
+
+  if (OrthancPluginHasInstanceMetadata(context, instance, "ReceptionDate"))
+  {
+    printf("Received on [%s]\n", OrthancPluginGetInstanceMetadata(context, instance, "ReceptionDate"));
+  }
+  else
+  {
+    OrthancPluginLogError(context, "Instance has no reception date, should never happen!");
+  }
+
+  return 0;
+}
+
+
+ORTHANC_PLUGINS_API int32_t OnChangeCallback(OrthancPluginChangeType changeType,
+                                             OrthancPluginResourceType resourceType,
+                                             const char* resourceId)
+{
+  char info[1024];
+  OrthancPluginMemoryBuffer tmp;
+
+  sprintf(info, "Change %d on resource %s of type %d", changeType, resourceId, resourceType);
+  OrthancPluginLogWarning(context, info);
+
+  if (changeType == 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);
+    }
+  }
+
+  return 0;
+}
+
+
+ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+{
+  OrthancPluginMemoryBuffer tmp;
+  char info[1024], *s;
+  int counter, i;
+
+  context = c;
+  OrthancPluginLogWarning(context, "Sample plugin is initializing");
+
+  /* Check the version of the Orthanc core */
+  if (OrthancPluginCheckVersion(c) == 0)
+  {
+    sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+            c->orthancVersion,
+            ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+            ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+            ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+    OrthancPluginLogError(context, info);
+    return -1;
+  }
+
+  /* Print some information about Orthanc */
+  sprintf(info, "The version of Orthanc is '%s'", context->orthancVersion);
+  OrthancPluginLogWarning(context, info);
+
+  s = OrthancPluginGetOrthancPath(context);
+  sprintf(info, "  Path to Orthanc: %s", s);
+  OrthancPluginLogWarning(context, info);
+  OrthancPluginFreeString(context, s);
+
+  s = OrthancPluginGetOrthancDirectory(context);
+  sprintf(info, "  Directory of Orthanc: %s", s);
+  OrthancPluginLogWarning(context, info);
+  OrthancPluginFreeString(context, s);
+
+  s = OrthancPluginGetConfigurationPath(context);
+  sprintf(info, "  Path to configuration file: %s", s);
+  OrthancPluginLogWarning(context, info);
+  OrthancPluginFreeString(context, s);
+
+  /* Print the command-line arguments of Orthanc */
+  counter = OrthancPluginGetCommandLineArgumentsCount(context);
+  for (i = 0; i < counter; i++)
+  {
+    s = OrthancPluginGetCommandLineArgument(context, i);
+    sprintf(info, "  Command-line argument %d: \"%s\"", i, s);
+    OrthancPluginLogWarning(context, info);
+    OrthancPluginFreeString(context, s);    
+  }
+
+  /* Register the callbacks */
+  OrthancPluginRegisterRestCallback(context, "/(plu.*)/hello", Callback1);
+  OrthancPluginRegisterRestCallback(context, "/plu.*/image", Callback2);
+  OrthancPluginRegisterRestCallback(context, "/plugin/instances/([^/]+)/info", Callback3);
+  OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/preview", Callback4);
+  OrthancPluginRegisterRestCallback(context, "/forward/(built-in)(/.+)", Callback5);
+  OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5);
+  OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom);
+
+  OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback);
+
+  OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
+
+  /* Declare several properties of the plugin */
+  OrthancPluginSetRootUri(context, "/plugin/hello");
+  OrthancPluginSetDescription(context, "This is the description of the sample plugin that can be seen in Orthanc Explorer.");
+  OrthancPluginExtendOrthancExplorer(context, "alert('Hello Orthanc! From sample plugin with love.');");
+
+  /* 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));
+
+  /* Play with global properties: A global counter is incremented 
+     each time the plugin starts. */
+  s = OrthancPluginGetGlobalProperty(context, 1024, "0");
+  sscanf(s, "%d", &counter);
+  sprintf(info, "Number of times this plugin was started: %d", counter);
+  OrthancPluginLogWarning(context, info);
+  counter++;
+  sprintf(info, "%d", counter);
+  OrthancPluginSetGlobalProperty(context, 1024, info);
+  OrthancPluginFreeString(context, s);
+
+  return 0;
+}
+
+
+ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+{
+  OrthancPluginLogWarning(context, "Sample plugin is finalizing");
+}
+
+
+ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+{
+  return "sample";
+}
+
+
+ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+{
+  return "1.0";
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoding/CMakeLists.txt	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,56 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(GdcmDecoding)
+
+SET(ALLOW_DOWNLOADS ON CACHE BOOL "Allow CMake to download packages")
+SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost")
+SET(USE_SYSTEM_GOOGLE_LOG OFF CACHE BOOL "Use the system version of Google Log")
+
+set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../..)
+
+if (${CMAKE_COMPILER_IS_GNUCXX})
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
+  set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
+endif()
+
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfiguration.cmake)
+
+find_package(GDCM REQUIRED)
+if (GDCM_FOUND)
+  include(${GDCM_USE_FILE})
+  set(GDCM_LIBRARIES gdcmCommon gdcmMSFF)
+else(GDCM_FOUND)
+  message(FATAL_ERROR "Cannot find GDCM, did you set GDCM_DIR?")
+endif(GDCM_FOUND)
+
+include_directories(
+  ${ORTHANC_ROOT}/Plugins/Include/
+  ${ORTHANC_ROOT}/OrthancCppClient/SharedLibrary/Laaw
+  )
+
+add_library(GdcmDecoding SHARED
+  Plugin.cpp
+  OrthancContext.cpp
+
+  # Sources from Orthanc
+  ${GOOGLE_LOG_SOURCES}
+  ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp
+  ${ORTHANC_ROOT}/Core/Enumerations.cpp
+  ${ORTHANC_ROOT}/Core/ImageFormats/ImageAccessor.cpp
+  ${ORTHANC_ROOT}/Core/ImageFormats/ImageBuffer.cpp
+  ${ORTHANC_ROOT}/Core/ImageFormats/ImageProcessing.cpp
+  ${ORTHANC_ROOT}/Core/OrthancException.cpp
+  ${ORTHANC_ROOT}/Core/Toolbox.cpp
+  ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp
+  ${ORTHANC_ROOT}/Resources/ThirdParty/md5/md5.c
+  ${THIRD_PARTY_SOURCES}
+  )
+
+target_link_libraries(GdcmDecoding ${GDCM_LIBRARIES})
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+  target_link_libraries(GdcmDecoding pthread dl rt)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoding/OrthancContext.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,158 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+#include "OrthancContext.h"
+
+#include <stdexcept>
+
+
+void OrthancContext::Check()
+{
+  if (context_ == NULL)
+  {
+    throw std::runtime_error("The Orthanc plugin context is not initialized");
+  }
+}
+
+
+OrthancContext& OrthancContext::GetInstance()
+{
+  static OrthancContext instance;
+  return instance;
+}
+
+
+void OrthancContext::ExtractGetArguments(Arguments& arguments,
+                                         const OrthancPluginHttpRequest& request)
+{
+  Check();
+  arguments.clear();
+
+  for (uint32_t i = 0; i < request.getCount; i++)
+  {
+    arguments[request.getKeys[i]] = request.getValues[i];
+  }
+}
+
+
+void OrthancContext::LogError(const std::string& s)
+{
+  Check();
+  OrthancPluginLogError(context_, s.c_str());
+}
+
+
+void OrthancContext::LogWarning(const std::string& s)
+{
+  Check();
+  OrthancPluginLogWarning(context_, s.c_str());
+}
+
+
+void OrthancContext::LogInfo(const std::string& s)
+{
+  Check();
+  OrthancPluginLogInfo(context_, s.c_str());
+}
+  
+
+void OrthancContext::Register(const std::string& uri,
+                              OrthancPluginRestCallback callback)
+{
+  Check();
+  OrthancPluginRegisterRestCallback(context_, uri.c_str(), callback);
+}
+
+
+void OrthancContext::GetDicomForInstance(std::string& result,
+                                         const std::string& instanceId)
+{
+  Check();
+  OrthancPluginMemoryBuffer buffer;
+    
+  if (OrthancPluginGetDicomForInstance(context_, &buffer, instanceId.c_str()))
+  {
+    throw std::runtime_error("No DICOM instance with Orthanc ID: " + instanceId);
+  }
+
+  if (buffer.size == 0)
+  {
+    result.clear();
+  }
+  else
+  {
+    result.assign(reinterpret_cast<char*>(buffer.data), buffer.size);
+  }
+
+  OrthancPluginFreeMemoryBuffer(context_, &buffer);
+}
+
+
+void OrthancContext::CompressAndAnswerPngImage(OrthancPluginRestOutput* output,
+                                               const Orthanc::ImageAccessor& accessor)
+{
+  Check();
+
+  OrthancPluginPixelFormat format;
+  switch (accessor.GetFormat())
+  {
+    case Orthanc::PixelFormat_Grayscale8:
+      format = OrthancPluginPixelFormat_Grayscale8;
+      break;
+
+    case Orthanc::PixelFormat_Grayscale16:
+      format = OrthancPluginPixelFormat_Grayscale16;
+      break;
+
+    case Orthanc::PixelFormat_SignedGrayscale16:
+      format = OrthancPluginPixelFormat_SignedGrayscale16;
+      break;
+
+    case Orthanc::PixelFormat_RGB24:
+      format = OrthancPluginPixelFormat_RGB24;
+      break;
+
+    case Orthanc::PixelFormat_RGBA32:
+      format = OrthancPluginPixelFormat_RGBA32;
+      break;
+
+    default:
+      throw std::runtime_error("Unsupported pixel format");
+  }
+
+  OrthancPluginCompressAndAnswerPngImage(context_, output, format, accessor.GetWidth(),
+                                         accessor.GetHeight(), accessor.GetPitch(), accessor.GetConstBuffer());
+}
+
+
+
+void OrthancContext::Redirect(OrthancPluginRestOutput* output,
+                              const std::string& s)
+{
+  Check();
+  OrthancPluginRedirect(context_, output, s.c_str());
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoding/OrthancContext.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,85 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+#pragma once
+
+#include <OrthancCPlugin.h>
+
+#include "../../../Core/ImageFormats/ImageBuffer.h"
+
+#include <map>
+#include <string>
+#include <boost/noncopyable.hpp>
+
+
+class OrthancContext : public boost::noncopyable
+{
+private:
+  OrthancPluginContext* context_;
+
+  OrthancContext() : context_(NULL)
+  {
+  }
+
+  void Check();
+
+public:
+  typedef std::map<std::string, std::string>  Arguments;
+
+  static OrthancContext& GetInstance();
+
+  void Initialize(OrthancPluginContext* context)
+  {
+    context_ = context;
+  }
+
+  void Finalize()
+  {
+    context_ = NULL;
+  }
+
+  void ExtractGetArguments(Arguments& arguments,
+                           const OrthancPluginHttpRequest& request);
+
+  void LogError(const std::string& s);
+
+  void LogWarning(const std::string& s);
+
+  void LogInfo(const std::string& s);
+  
+  void Register(const std::string& uri,
+                OrthancPluginRestCallback callback);
+
+  void GetDicomForInstance(std::string& result,
+                           const std::string& instanceId);
+
+  void CompressAndAnswerPngImage(OrthancPluginRestOutput* output,
+                                 const Orthanc::ImageAccessor& accessor);
+
+  void Redirect(OrthancPluginRestOutput* output,
+                const std::string& s);
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/GdcmDecoding/Plugin.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,271 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+#include <map>
+#include <string>
+#include <stdexcept>
+#include <sstream>
+#include <boost/lexical_cast.hpp>
+
+#include "OrthancContext.h"
+#include "../../../Core/ImageFormats/ImageProcessing.h"
+
+#include <gdcmReader.h>
+#include <gdcmImageReader.h>
+#include <gdcmImageChangePlanarConfiguration.h>
+
+
+static void AnswerUnsupportedImage(OrthancPluginRestOutput* output)
+{
+  OrthancContext::GetInstance().Redirect(output, "/app/images/unsupported.png");
+}
+
+
+static bool GetOrthancPixelFormat(Orthanc::PixelFormat& format,
+                                  const gdcm::Image& image)
+{
+  if (image.GetPlanarConfiguration() != 0 && 
+      image.GetPixelFormat().GetSamplesPerPixel() != 1)
+  {
+    OrthancContext::GetInstance().LogError("Planar configurations are not supported");
+    return false;
+  }
+
+  if (image.GetPixelFormat().GetSamplesPerPixel() == 1)
+  {
+    switch (image.GetPixelFormat().GetScalarType())
+    {
+      case gdcm::PixelFormat::UINT8:
+        format = Orthanc::PixelFormat_Grayscale8;
+        return true;
+
+      case gdcm::PixelFormat::UINT16:
+        format = Orthanc::PixelFormat_Grayscale16;
+        return true;
+
+      case gdcm::PixelFormat::INT16:
+        format = Orthanc::PixelFormat_SignedGrayscale16;
+        return true;
+
+      default:
+        return false;
+    }
+  }
+  else if (image.GetPixelFormat().GetSamplesPerPixel() == 3 &&
+           image.GetPixelFormat().GetScalarType() == gdcm::PixelFormat::UINT8)
+  {
+    format = Orthanc::PixelFormat_RGB24;
+    return true;
+  }
+  else if (image.GetPixelFormat().GetSamplesPerPixel() == 4 &&
+           image.GetPixelFormat().GetScalarType() == gdcm::PixelFormat::UINT8)
+  {
+    format = Orthanc::PixelFormat_RGBA32;
+    return true;
+  }
+  
+  return false;
+}
+
+
+ORTHANC_PLUGINS_API int32_t DecodeImage(OrthancPluginRestOutput* output,
+                                        const char* url,
+                                        const OrthancPluginHttpRequest* request)
+{
+  std::string instance(request->groups[0]);
+  std::string outputFormat(request->groups[1]);
+  OrthancContext::GetInstance().LogWarning("Using GDCM to decode instance " + instance);
+
+  // Download the request DICOM instance from Orthanc into a memory buffer
+  std::string dicom;
+  OrthancContext::GetInstance().GetDicomForInstance(dicom, instance);
+
+  // Prepare a memory stream over the DICOM instance
+  std::stringstream stream(dicom);
+
+  // Parse the DICOM instance using GDCM
+  gdcm::ImageReader imageReader;
+  imageReader.SetStream(stream);
+  if (!imageReader.Read())
+  {
+    OrthancContext::GetInstance().LogError("GDCM cannot extract an image from this DICOM instance");
+    AnswerUnsupportedImage(output);
+    return 0;
+  }
+
+  gdcm::Image& image = imageReader.GetImage();
+
+
+  // Log information about the decoded image
+  char tmp[1024];
+  sprintf(tmp, "Image format: %dx%d %s with %d color channel(s)", image.GetRows(), image.GetColumns(), 
+          image.GetPixelFormat().GetScalarTypeAsString(), image.GetPixelFormat().GetSamplesPerPixel());
+  OrthancContext::GetInstance().LogWarning(tmp);
+
+
+  // Convert planar configuration
+  gdcm::ImageChangePlanarConfiguration planar;
+  if (image.GetPlanarConfiguration() != 0 && 
+      image.GetPixelFormat().GetSamplesPerPixel() != 1)
+  {
+    OrthancContext::GetInstance().LogWarning("Converting planar configuration to interleaved");
+    planar.SetInput(imageReader.GetImage());
+    planar.Change();
+    image = planar.GetOutput();
+  }
+
+
+  // Create a read-only accessor to the bitmap decoded by GDCM
+  Orthanc::PixelFormat format;
+  if (!GetOrthancPixelFormat(format, image))
+  {
+    OrthancContext::GetInstance().LogError("This sample plugin does not support this image format");
+    AnswerUnsupportedImage(output);
+    return 0;
+  }
+
+  Orthanc::ImageAccessor decodedImage;
+  std::vector<char> decodedBuffer(image.GetBufferLength());
+
+  if (decodedBuffer.size())
+  {
+    image.GetBuffer(&decodedBuffer[0]);
+    unsigned int pitch = image.GetColumns() * ::Orthanc::GetBytesPerPixel(format);
+    decodedImage.AssignWritable(format, image.GetColumns(), image.GetRows(), pitch, &decodedBuffer[0]);
+  }
+  else
+  {
+    // Empty image
+    decodedImage.AssignWritable(format, 0, 0, 0, NULL);
+  }
+
+
+  // Convert the pixel format from GDCM to the format requested by the REST query
+  Orthanc::ImageBuffer converted;
+  converted.SetWidth(decodedImage.GetWidth());
+  converted.SetHeight(decodedImage.GetHeight());
+
+  if (outputFormat == "preview")
+  {
+    if (format == Orthanc::PixelFormat_RGB24 ||
+        format == Orthanc::PixelFormat_RGBA32)
+    {
+      // Do not rescale color image
+      converted.SetFormat(Orthanc::PixelFormat_RGB24);
+    }
+    else
+    {
+      converted.SetFormat(Orthanc::PixelFormat_Grayscale8);
+
+      // Rescale the image to the [0,255] range
+      int64_t a, b;
+      Orthanc::ImageProcessing::GetMinMaxValue(a, b, decodedImage);
+
+      float offset = -a;
+      float scaling = 255.0f / static_cast<float>(b - a);
+      Orthanc::ImageProcessing::ShiftScale(decodedImage, offset, scaling);
+    }
+  }
+  else
+  {
+    if (format == Orthanc::PixelFormat_RGB24 ||
+        format == Orthanc::PixelFormat_RGBA32)
+    {
+      // Do not convert color images to grayscale values (this is Orthanc convention)
+      AnswerUnsupportedImage(output);
+      return 0;
+    }
+
+    if (outputFormat == "image-uint8")
+    {
+      converted.SetFormat(Orthanc::PixelFormat_Grayscale8);
+    }
+    else if (outputFormat == "image-uint16")
+    {
+      converted.SetFormat(Orthanc::PixelFormat_Grayscale16);
+    }
+    else if (outputFormat == "image-int16")
+    {
+      converted.SetFormat(Orthanc::PixelFormat_SignedGrayscale16);
+    }
+    else
+    {
+      OrthancContext::GetInstance().LogError("Unknown output format: " + outputFormat);
+      AnswerUnsupportedImage(output);
+      return 0;
+    }
+  }
+
+  Orthanc::ImageAccessor convertedAccessor(converted.GetAccessor());
+  Orthanc::ImageProcessing::Convert(convertedAccessor, decodedImage);
+
+  // Compress the converted image as a PNG file
+  OrthancContext::GetInstance().CompressAndAnswerPngImage(output, convertedAccessor);
+
+  return 0;  // Success
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    OrthancContext::GetInstance().Initialize(context);
+    OrthancContext::GetInstance().LogWarning("Initializing GDCM decoding");
+
+    // Check the version of the Orthanc core
+    if (OrthancPluginCheckVersion(context) == 0)
+    {
+      OrthancContext::GetInstance().LogError(
+        "Your version of Orthanc (" + std::string(context->orthancVersion) +
+        ") must be above " + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER) +
+        "." + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER) +
+        "." + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER) +
+        " to run this plugin");
+      return -1;
+    }
+
+    OrthancContext::GetInstance().Register("/instances/([^/]+)/(preview|image-uint8|image-uint16|image-int16)", DecodeImage);
+    return 0;
+  }
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancContext::GetInstance().LogWarning("Finalizing GDCM decoding");
+    OrthancContext::GetInstance().Finalize();
+  }
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "gdcm-decoding";
+  }
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return "1.0";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ServeFolders/CMakeLists.txt	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,35 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(ServeFolders)
+
+set(ALLOW_DOWNLOADS ON)
+set(USE_SYSTEM_JSONCPP OFF)
+include(../../../Resources/CMake/DownloadPackage.cmake)
+include(../../../Resources/CMake/JsonCppConfiguration.cmake)
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+  link_libraries(uuid)
+  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pthread")
+elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  link_libraries(rpcrt4 ws2_32 secur32)
+  if (CMAKE_COMPILER_IS_GNUCXX)
+    SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libgcc -static-libstdc++")
+  endif()
+endif ()
+
+if (CMAKE_COMPILER_IS_GNUCXX)
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=${CMAKE_SOURCE_DIR}/VersionScript.map -Wl,--no-undefined")
+endif()
+
+include_directories(${CMAKE_SOURCE_DIR}/../../Include/)
+add_library(ServeFolders SHARED 
+  Plugin.cpp
+  ${THIRD_PARTY_SOURCES}
+  )
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+  # Linking with "pthread" is necessary, otherwise the software crashes
+  # http://sourceware.org/bugzilla/show_bug.cgi?id=10652#c17
+  target_link_libraries(ServeFolders dl rt)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ServeFolders/Plugin.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,326 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+#include <OrthancCPlugin.h>
+
+#include <json/reader.h>
+#include <json/value.h>
+#include <string.h>
+#include <stdio.h>
+#include <fstream>
+#include <algorithm>
+#include <sys/stat.h>
+
+static OrthancPluginContext* context_ = NULL;
+static std::map<std::string, std::string> folders_;
+static const char* INDEX_URI = "/app/plugin-serve-folders.html";
+
+
+static 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 == ".png")
+  {
+    return "image/png";
+  }
+  else if (extension == ".jpg" || extension == ".jpeg")
+  {
+    return "image/jpeg";
+  }
+  else
+  {
+    std::string s = "Unknown MIME type for extension: " + extension;
+    OrthancPluginLogWarning(context_, s.c_str());
+    return "application/octet-stream";
+  }
+}
+
+
+
+static bool ReadFile(std::string& content,
+                     const std::string& path)
+{
+  struct stat s;
+  if (stat(path.c_str(), &s) != 0 ||
+      !(s.st_mode & S_IFREG))
+  {
+    // Either the path does not exist, or it is not a regular file
+    return false;
+  }
+
+  FILE* fp = fopen(path.c_str(), "rb");
+  if (fp == NULL)
+  {
+    return false;
+  }
+
+  long size;
+
+  if (fseek(fp, 0, SEEK_END) == -1 ||
+      (size = ftell(fp)) < 0)
+  {
+    fclose(fp);
+    return false;
+  }
+
+  content.resize(size);
+      
+  if (fseek(fp, 0, SEEK_SET) == -1)
+  {
+    fclose(fp);
+    return false;
+  }
+
+  bool ok = true;
+
+  if (size > 0 &&
+      fread(&content[0], size, 1, fp) != 1)
+  {
+    ok = false;
+  }
+
+  fclose(fp);
+
+  return ok;
+}
+
+
+static bool ReadConfiguration(Json::Value& configuration,
+                              OrthancPluginContext* context)
+{
+  std::string path;
+
+  {
+    char* pathTmp = OrthancPluginGetConfigurationPath(context);
+    if (pathTmp == NULL)
+    {
+      OrthancPluginLogError(context, "No configuration file is provided");
+      return false;
+    }
+
+    path = std::string(pathTmp);
+
+    OrthancPluginFreeString(context, pathTmp);
+  }
+
+  std::ifstream f(path.c_str());
+
+  Json::Reader reader;
+  if (!reader.parse(f, configuration) ||
+      configuration.type() != Json::objectValue)
+  {
+    std::string s = "Unable to parse the configuration file: " + std::string(path);
+    OrthancPluginLogError(context, s.c_str());
+    return false;
+  }
+
+  return true;
+}
+
+
+static int32_t FolderCallback(OrthancPluginRestOutput* output,
+                              const char* url,
+                              const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    return 0;
+  }
+
+  const std::string uri = request->groups[0];
+  const std::string item = request->groups[1];
+
+  std::map<std::string, std::string>::const_iterator found = folders_.find(uri);
+  if (found == folders_.end())
+  {
+    std::string s = "Unknown URI in plugin server-folders: " + uri;
+    OrthancPluginLogError(context_, s.c_str());
+    OrthancPluginSendHttpStatusCode(context_, output, 404);
+    return 0;
+  }
+
+  std::string path = found->second + "/" + item;
+  const char* mime = GetMimeType(path);
+
+  std::string s;
+  if (ReadFile(s, path))
+  {
+    const char* resource = s.size() ? s.c_str() : NULL;
+    OrthancPluginAnswerBuffer(context_, output, resource, s.size(), mime);
+  }
+  else
+  {
+    std::string s = "Inexistent file in served folder: " + path;
+    OrthancPluginLogError(context_, s.c_str());
+    OrthancPluginSendHttpStatusCode(context_, output, 404);
+  }
+
+  return 0;
+}
+
+
+static int32_t IndexCallback(OrthancPluginRestOutput* output,
+                             const char* url,
+                             const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    return 0;
+  }
+
+  std::string s = "<html><body><h1>Additional folders served by Orthanc</h1><ul>\n";
+
+  for (std::map<std::string, std::string>::const_iterator
+         it = folders_.begin(); it != folders_.end(); it++)
+  {
+    s += "<li><a href=\"" + it->first + "/index.html\">" + it->first + "</li>\n";
+  }
+
+  s += "</ul></body></html>";
+
+  OrthancPluginAnswerBuffer(context_, output, s.c_str(), s.size(), "text/html");
+
+  return 0;
+}
+
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    context_ = context;
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(context_) == 0)
+    {
+      char info[1024];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              context_->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(context_, info);
+      return -1;
+    }
+
+    OrthancPluginSetDescription(context_, "Serve additional folders with the HTTP server of Orthanc.");
+
+    Json::Value configuration;
+    if (!ReadConfiguration(configuration, context_))
+    {
+      return -1;
+    }
+
+    if (configuration.isMember("ServeFolders") &&
+        configuration["ServeFolders"].type() == Json::objectValue)
+    {
+      Json::Value::Members members = configuration["ServeFolders"].getMemberNames();
+
+      // Register the callback for each base URI
+      for (Json::Value::Members::const_iterator 
+             it = members.begin(); it != members.end(); it++)
+      {
+        const std::string& baseUri = *it;
+        const std::string path = configuration["ServeFolders"][*it].asString();
+        const std::string regex = "(" + baseUri + ")/(.*)";
+
+        if (baseUri.empty() ||
+            *baseUri.rbegin() == '/')
+        {
+          std::string message = "The URI of a folder to be server cannot be empty or end with a '/': " + *it;
+          OrthancPluginLogWarning(context_, message.c_str());
+          return -1;
+        }
+
+        OrthancPluginRegisterRestCallback(context, regex.c_str(), FolderCallback);
+        folders_[baseUri] = path;
+      }
+
+      OrthancPluginRegisterRestCallback(context, INDEX_URI, IndexCallback);
+      OrthancPluginSetRootUri(context, INDEX_URI);
+    }
+    else
+    {
+      OrthancPluginLogWarning(context_, "No section \"ServeFolders\" in your configuration file: "
+                              "No additional folder will be served!");
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "serve-folders";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return "1.0";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ServeFolders/README	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,52 @@
+Introduction
+============
+
+This sample plugin enables Orthanc to serve additional folders using
+its embedded Web server.
+
+
+Compilation for Linux
+=====================
+
+# mkdir Build
+# cd Build
+# cmake ..
+# make
+
+
+Cross-compilation for Windows using MinGW
+=========================================
+
+# mkdir Build
+# cd Build
+# cmake .. -DCMAKE_TOOLCHAIN_FILE=../../../Resources/MinGWToolchain.cmake
+# make
+
+
+Configuration
+=============
+
+First, generate the default configuration of Orthanc:
+https://code.google.com/p/orthanc/wiki/OrthancConfiguration
+
+Then, modify the "Plugins" option to point to the folder containing
+the built plugins.
+
+Finally, create a section "ServeFolders" in the configuration file to
+specify which folder you want to serve, and at which URI. For
+instance, the following excerpt would load the plugins from the
+working directory, then would branch the content of the folder
+"/home/jodogne/WWW/fosdem" as the URI "http://localhost:8042/fosdem":
+
+{
+  "Name" : "MyOrthanc",
+  [...]
+  "HttpPort" : 8042,
+  [...]
+  "Plugins" : [ 
+    "."
+  ],
+  "ServeFolders" : {
+    "/fosdem" : "/home/jodogne/WWW/fosdem"
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/ServeFolders/VersionScript.map	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,12 @@
+# This is a version-script for Orthanc plugins
+
+{
+global:
+  OrthancPluginInitialize;
+  OrthancPluginFinalize;
+  OrthancPluginGetName;
+  OrthancPluginGetVersion;
+
+local:
+  *;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/StorageArea/CMakeLists.txt	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(Basic)
+
+if (${CMAKE_COMPILER_IS_GNUCXX})
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic -Werror")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Werror")
+endif()
+
+include_directories(${CMAKE_SOURCE_DIR}/../../Include/)
+add_library(PluginTest SHARED Plugin.cpp)
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+  # Linking with "pthread" is necessary, otherwise the software crashes
+  # http://sourceware.org/bugzilla/show_bug.cgi?id=10652#c17
+  target_link_libraries(PluginTest pthread dl)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/StorageArea/Plugin.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,162 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+#include <OrthancCPlugin.h>
+
+#include <string.h>
+#include <stdio.h>
+#include <string>
+
+static OrthancPluginContext* context = NULL;
+
+
+static std::string GetPath(const char* uuid)
+{
+  return "plugin_" + std::string(uuid);
+}
+
+
+static int32_t StorageCreate(const char* uuid,
+                             const void* content,
+                             int64_t size,
+                             OrthancPluginContentType type)
+{
+  std::string path = GetPath(uuid);
+
+  FILE* fp = fopen(path.c_str(), "wb");
+  if (!fp)
+  {
+    return -1;
+  }
+
+  bool ok = fwrite(content, size, 1, fp) == 1;
+  fclose(fp);
+
+  return ok ? 0 : -1;
+}
+
+
+static int32_t StorageRead(void** content,
+                           int64_t* size,
+                           const char* uuid,
+                           OrthancPluginContentType type)
+{
+  std::string path = GetPath(uuid);
+
+  FILE* fp = fopen(path.c_str(), "rb");
+  if (!fp)
+  {
+    return -1;
+  }
+
+  if (fseek(fp, 0, SEEK_END) < 0)
+  {
+    fclose(fp);
+    return -1;
+  }
+
+  *size = ftell(fp);
+
+  if (fseek(fp, 0, SEEK_SET) < 0)
+  {
+    fclose(fp);
+    return -1;
+  }
+
+  bool ok = true;
+
+  if (*size == 0)
+  {
+    *content = NULL;
+  }
+  else
+  {
+    *content = malloc(*size);
+    if (*content == NULL ||
+        fread(*content, *size, 1, fp) != 1)
+    {
+      ok = false;
+    }
+  }
+
+  fclose(fp);
+
+  return ok ? 0 : -1;  
+}
+
+
+static int32_t StorageRemove(const char* uuid,
+                             OrthancPluginContentType type)
+{
+  std::string path = GetPath(uuid);
+  return remove(path.c_str());
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    context = c;
+    OrthancPluginLogWarning(context, "Storage plugin is initializing");
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      char info[1024];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              c->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(context, info);
+      return -1;
+    }
+
+    OrthancPluginRegisterStorageArea(context, StorageCreate, StorageRead, StorageRemove);
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancPluginLogWarning(context, "Storage plugin is finalizing");
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "storage";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return "1.0";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/CMakeLists.txt	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,14 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(WebSkeleton)
+
+SET(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)")
+SET(RESOURCES_ROOT ${CMAKE_SOURCE_DIR}/StaticResources)
+
+include(Framework/Framework.cmake)
+
+include_directories(${CMAKE_SOURCE_DIR}/../../Include/)
+
+add_library(WebSkeleton SHARED 
+  ${AUTOGENERATED_SOURCES}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/Configuration.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,34 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+#pragma once
+
+#define ORTHANC_PLUGIN_NAME  "web-skeleton"
+
+#define ORTHANC_PLUGIN_VERSION "1.0"
+
+#define ORTHANC_PLUGIN_WEB_ROOT  "/plugin-web-skeleton/"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,398 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, 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/>.
+
+
+import sys
+import os
+import os.path
+import pprint
+import re
+
+UPCASE_CHECK = True
+ARGS = []
+for i in range(len(sys.argv)):
+    if not sys.argv[i].startswith('--'):
+        ARGS.append(sys.argv[i])
+    elif sys.argv[i].lower() == '--no-upcase-check':
+        UPCASE_CHECK = False
+
+if len(ARGS) < 2 or len(ARGS) % 2 != 0:
+    print ('Usage:')
+    print ('python %s [--no-upcase-check] <TargetBaseFilename> [ <Name> <Source> ]*' % sys.argv[0])
+    exit(-1)
+
+TARGET_BASE_FILENAME = ARGS[1]
+SOURCES = ARGS[2:]
+
+try:
+    # Make sure the destination directory exists
+    os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..')))
+except:
+    pass
+
+
+#####################################################################
+## Read each resource file
+#####################################################################
+
+def CheckNoUpcase(s):
+    global UPCASE_CHECK
+    if (UPCASE_CHECK and
+        re.search('[A-Z]', s) != None):
+        raise Exception("Path in a directory with an upcase letter: %s" % s)
+
+resources = {}
+
+counter = 0
+i = 0
+while i < len(SOURCES):
+    resourceName = SOURCES[i].upper()
+    pathName = SOURCES[i + 1]
+
+    if not os.path.exists(pathName):
+        raise Exception("Non existing path: %s" % pathName)
+
+    if resourceName in resources:
+        raise Exception("Twice the same resource: " + resourceName)
+    
+    if os.path.isdir(pathName):
+        # The resource is a directory: Recursively explore its files
+        content = {}
+        for root, dirs, files in os.walk(pathName):
+            base = os.path.relpath(root, pathName)
+            for f in files:
+                if f.find('~') == -1:  # Ignore Emacs backup files
+                    if base == '.':
+                        r = f
+                    else:
+                        r = os.path.join(base, f)
+
+                    CheckNoUpcase(r)
+                    r = '/' + r.replace('\\', '/')
+                    if r in content:
+                        raise Exception("Twice the same filename (check case): " + r)
+
+                    content[r] = {
+                        'Filename' : os.path.join(root, f),
+                        'Index' : counter
+                        }
+                    counter += 1
+
+        resources[resourceName] = {
+            'Type' : 'Directory',
+            'Files' : content
+            }
+
+    elif os.path.isfile(pathName):
+        resources[resourceName] = {
+            'Type' : 'File',
+            'Index' : counter,
+            'Filename' : pathName
+            }
+        counter += 1
+
+    else:
+        raise Exception("Not a regular file, nor a directory: " + pathName)
+
+    i += 2
+
+#pprint.pprint(resources)
+
+
+#####################################################################
+## Write .h header
+#####################################################################
+
+header = open(TARGET_BASE_FILENAME + '.h', 'w')
+
+header.write("""
+#pragma once
+
+#include <string>
+#include <list>
+
+namespace Orthanc
+{
+  namespace EmbeddedResources
+  {
+    enum FileResourceId
+    {
+""")
+
+isFirst = True
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        if isFirst:
+            isFirst = False
+        else:    
+            header.write(',\n')
+        header.write('      %s' % name)
+
+header.write("""
+    };
+
+    enum DirectoryResourceId
+    {
+""")
+
+isFirst = True
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        if isFirst:
+            isFirst = False
+        else:    
+            header.write(',\n')
+        header.write('      %s' % name)
+
+header.write("""
+    };
+
+    const void* GetFileResourceBuffer(FileResourceId id);
+    size_t GetFileResourceSize(FileResourceId id);
+    void GetFileResource(std::string& result, FileResourceId id);
+
+    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path);
+    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path);
+    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path);
+
+    void ListResources(std::list<std::string>& result, DirectoryResourceId id);
+  }
+}
+""")
+header.close()
+
+
+
+#####################################################################
+## Write the resource content in the .cpp source
+#####################################################################
+
+PYTHON_MAJOR_VERSION = sys.version_info[0]
+
+def WriteResource(cpp, item):
+    cpp.write('    static const uint8_t resource%dBuffer[] = {' % item['Index'])
+
+    f = open(item['Filename'], "rb")
+    content = f.read()
+    f.close()
+
+    # http://stackoverflow.com/a/1035360
+    pos = 0
+    for b in content:
+        if PYTHON_MAJOR_VERSION == 2:
+            c = ord(b[0])
+        else:
+            c = b
+
+        if pos > 0:
+            cpp.write(', ')
+
+        if (pos % 16) == 0:
+            cpp.write('\n    ')
+
+        if c < 0:
+            raise Exception("Internal error")
+
+        cpp.write("0x%02x" % c)
+        pos += 1
+
+    cpp.write('  };\n')
+    cpp.write('    static const size_t resource%dSize = %d;\n' % (item['Index'], pos))
+
+
+cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w')
+
+print os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+
+cpp.write("""
+#include "%s.h"
+
+#include <stdint.h>
+#include <string.h>
+#include <stdexcept>
+
+namespace Orthanc
+{
+  namespace EmbeddedResources
+  {
+""" % (os.path.basename(TARGET_BASE_FILENAME)))
+
+
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        WriteResource(cpp, resources[name])
+    else:
+        for f in resources[name]['Files']:
+            WriteResource(cpp, resources[name]['Files'][f])
+
+
+
+#####################################################################
+## Write the accessors to the file resources in .cpp
+#####################################################################
+
+cpp.write("""
+    const void* GetFileResourceBuffer(FileResourceId id)
+    {
+      switch (id)
+      {
+""")
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        cpp.write('      case %s:\n' % name)
+        cpp.write('        return resource%dBuffer;\n' % resources[name]['Index'])
+
+cpp.write("""
+      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+
+    size_t GetFileResourceSize(FileResourceId id)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        cpp.write('      case %s:\n' % name)
+        cpp.write('        return resource%dSize;\n' % resources[name]['Index'])
+
+cpp.write("""
+      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+""")
+
+
+
+#####################################################################
+## Write the accessors to the directory resources in .cpp
+#####################################################################
+
+cpp.write("""
+    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        isFirst = True
+        for path in resources[name]['Files']:
+            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
+            cpp.write('          return resource%dBuffer;\n' % resources[name]['Files'][path]['Index'])
+        cpp.write('        throw std::runtime_error("Unknown path in a directory resource");\n\n')
+
+cpp.write("""      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+
+    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        isFirst = True
+        for path in resources[name]['Files']:
+            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
+            cpp.write('          return resource%dSize;\n' % resources[name]['Files'][path]['Index'])
+        cpp.write('        throw std::runtime_error("Unknown path in a directory resource");\n\n')
+
+cpp.write("""      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+""")
+
+
+
+
+#####################################################################
+## List the resources in a directory
+#####################################################################
+
+cpp.write("""
+    void ListResources(std::list<std::string>& result, DirectoryResourceId id)
+    {
+      result.clear();
+
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        for path in sorted(resources[name]['Files']):
+            cpp.write('        result.push_back("%s");\n' % path)
+        cpp.write('        break;\n\n')
+
+cpp.write("""      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+""")
+
+
+
+
+#####################################################################
+## Write the convenience wrappers in .cpp
+#####################################################################
+
+cpp.write("""
+    void GetFileResource(std::string& result, FileResourceId id)
+    {
+      size_t size = GetFileResourceSize(id);
+      result.resize(size);
+      if (size > 0)
+        memcpy(&result[0], GetFileResourceBuffer(id), size);
+    }
+
+    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path)
+    {
+      size_t size = GetDirectoryResourceSize(id, path);
+      result.resize(size);
+      if (size > 0)
+        memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size);
+    }
+  }
+}
+""")
+cpp.close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,76 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, 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/>.
+
+
+if (${CMAKE_COMPILER_IS_GNUCXX})
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Werror -Wno-unused-function")
+endif()
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+  # Linking with "pthread" is necessary, otherwise the software might crash
+  # http://sourceware.org/bugzilla/show_bug.cgi?id=10652#c17
+  link_libraries(pthread dl)
+endif()
+
+if (STANDALONE_BUILD)
+  add_definitions(-DORTHANC_PLUGIN_STANDALONE=1)
+
+  set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED")
+  set(AUTOGENERATED_SOURCES "${AUTOGENERATED_DIR}/EmbeddedResources.cpp")
+
+  file(MAKE_DIRECTORY ${AUTOGENERATED_DIR})
+  include_directories(${AUTOGENERATED_DIR})
+
+  set(TARGET_BASE "${AUTOGENERATED_DIR}/EmbeddedResources")
+  add_custom_command(
+    OUTPUT
+    "${AUTOGENERATED_DIR}/EmbeddedResources.h"
+    "${AUTOGENERATED_DIR}/EmbeddedResources.cpp"
+    COMMAND 
+    python
+    "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py"
+    "${AUTOGENERATED_DIR}/EmbeddedResources"
+    STATIC_RESOURCES
+    ${RESOURCES_ROOT}
+    DEPENDS
+    "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py"
+    ${STATIC_RESOURCES}
+    )
+
+else()
+  add_definitions(
+    -DORTHANC_PLUGIN_STANDALONE=0
+    -DORTHANC_PLUGIN_RESOURCES_ROOT="${RESOURCES_ROOT}"
+    )
+endif()
+
+
+list(APPEND AUTOGENERATED_SOURCES
+  "${CMAKE_CURRENT_SOURCE_DIR}/Framework/Plugin.cpp"
+  )
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,276 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+#include "../Configuration.h"
+
+#include <OrthancCPlugin.h>
+#include <string>
+#include <stdexcept>
+#include <algorithm>
+#include <sys/stat.h>
+
+#if ORTHANC_PLUGIN_STANDALONE == 1
+// This is an auto-generated file for standalone builds
+#include <EmbeddedResources.h>
+#endif
+
+static OrthancPluginContext* context = NULL;
+
+
+static 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 == ".json")
+  {
+    return "application/json";
+  }
+  else if (extension == ".xml")
+  {
+    return "application/xml";
+  }
+  else if (extension == ".png")
+  {
+    return "image/png";
+  }
+  else if (extension == ".jpg" || extension == ".jpeg")
+  {
+    return "image/jpeg";
+  }
+  else
+  {
+    std::string s = "Unknown MIME type for extension: " + extension;
+    OrthancPluginLogWarning(context, s.c_str());
+    return "application/octet-stream";
+  }
+}
+
+
+static bool ReadFile(std::string& content,
+                     const std::string& path)
+{
+  struct stat s;
+  if (stat(path.c_str(), &s) != 0 ||
+      !(s.st_mode & S_IFREG))
+  {
+    // Either the path does not exist, or it is not a regular file
+    return false;
+  }
+
+  FILE* fp = fopen(path.c_str(), "rb");
+  if (fp == NULL)
+  {
+    return false;
+  }
+
+  long size;
+
+  if (fseek(fp, 0, SEEK_END) == -1 ||
+      (size = ftell(fp)) < 0)
+  {
+    fclose(fp);
+    return false;
+  }
+
+  content.resize(size);
+      
+  if (fseek(fp, 0, SEEK_SET) == -1)
+  {
+    fclose(fp);
+    return false;
+  }
+
+  bool ok = true;
+
+  if (size > 0 &&
+      fread(&content[0], size, 1, fp) != 1)
+  {
+    ok = false;
+  }
+
+  fclose(fp);
+
+  return ok;
+}
+
+
+#if ORTHANC_PLUGIN_STANDALONE == 1
+static int32_t ServeStaticResource(OrthancPluginRestOutput* output,
+                                   const char* url,
+                                   const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+    return 0;
+  }
+
+  std::string path = "/" + std::string(request->groups[0]);
+  const char* mime = GetMimeType(path);
+
+  try
+  {
+    std::string s;
+    Orthanc::EmbeddedResources::GetDirectoryResource
+      (s, Orthanc::EmbeddedResources::STATIC_RESOURCES, path.c_str());
+
+    const char* resource = s.size() ? s.c_str() : NULL;
+    OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime);
+
+    return 0;
+  }
+  catch (std::runtime_error&)
+  {
+    std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]);
+    OrthancPluginLogError(context, s.c_str());
+    OrthancPluginSendHttpStatusCode(context, output, 404);
+    return 0;
+  }
+}
+#endif
+
+
+#if ORTHANC_PLUGIN_STANDALONE == 0
+static int32_t ServeFolder(OrthancPluginRestOutput* output,
+                           const char* url,
+                           const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+    return 0;
+  }
+
+  std::string path = ORTHANC_PLUGIN_RESOURCES_ROOT "/" + std::string(request->groups[0]);
+  const char* mime = GetMimeType(path);
+
+  std::string s;
+  if (ReadFile(s, path))
+  {
+    const char* resource = s.size() ? s.c_str() : NULL;
+    OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime);
+
+    return 0;
+  }
+  else
+  {
+    std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]);
+    OrthancPluginLogError(context, s.c_str());
+    OrthancPluginSendHttpStatusCode(context, output, 404);
+    return 0;
+  }
+}
+#endif
+
+
+static int32_t RedirectRoot(OrthancPluginRestOutput* output,
+                            const char* url,
+                            const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    OrthancPluginRedirect(context, output, ORTHANC_PLUGIN_WEB_ROOT "index.html");
+  }
+
+  return 0;
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    context = c;
+    
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      char info[256];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              c->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(context, info);
+      return -1;
+    }
+
+    /* Register the callbacks */
+
+#if ORTHANC_PLUGIN_STANDALONE == 1
+    OrthancPluginLogInfo(context, "Serving static resources (standalone build)");
+    OrthancPluginRegisterRestCallback(context, ORTHANC_PLUGIN_WEB_ROOT "(.*)", ServeStaticResource);
+#else
+    OrthancPluginLogInfo(context, "Serving resources from folder: " ORTHANC_PLUGIN_RESOURCES_ROOT);
+    OrthancPluginRegisterRestCallback(context, ORTHANC_PLUGIN_WEB_ROOT "(.*)", ServeFolder);
+#endif
+
+    OrthancPluginRegisterRestCallback(context, "/", RedirectRoot);
+ 
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return ORTHANC_PLUGIN_NAME;
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/NOTES.txt	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,7 @@
+This is a sample Orthanc plugin that serves static resources (HTML,
+JavaScript, CSS, images...).
+
+The resources to serve must be stored in the folder "StaticResources".
+
+The folder "Framework" contains a reusable framework for any plugin
+whose sole objective is to serve static resources.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/WebSkeleton/StaticResources/index.html	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,16 @@
+<!doctype html>
+
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Web Skeleton</title>
+  </head>
+
+  <body>
+    <h1>Web Skeleton</h1>
+    <p>
+      This is a sample skeleton for Orthanc showing how to create a
+      plugin that serves static HTML resources.
+    </p>
+  </body>
+</html>
--- a/README	Wed Jun 25 15:34:40 2014 +0200
+++ b/README	Thu May 21 16:58:30 2015 +0200
@@ -41,11 +41,11 @@
 exception:
 http://people.gnome.org/~markmc/openssl-and-the-gpl.html
 
-We also kindly require scientific works and clinical studies that make
-use of Orthanc to cite Orthanc in their associated
-publications. Similarly, we require open-source and closed-source
-products that make use of Orthanc to warn us about this use. You can
-cite our work using the following BibTeX entry:
+We also kindly ask scientific works and clinical studies that make
+use of Orthanc to cite Orthanc in their associated publications.
+Similarly, we ask open-source and closed-source products that make
+use of Orthanc to warn us about this use. You can cite our work
+using the following BibTeX entry:
 
 @inproceedings{Jodogne:ISBI2013,
   author = {Jodogne, S. and Bernard, C. and Devillers, M. and Lenaerts, E. and Coucke, P.},
@@ -78,8 +78,9 @@
 * OrthancCppClient/   - Code of the C++ client
 * OrthancExplorer/    - Code of the Web application (HTML5/Javascript)
 * OrthancServer/      - Code of the Orthanc server (depends on DCMTK)
+* Plugins/            - Code of the plugin framework
 * Resources/          - Scripts, resources for building third-party code
-* UnitTests/          - Unit tests
+* UnitTestsSources/   - Unit tests
 
 This archive contains the following files:
 
--- a/Resources/CMake/AutoGeneratedCode.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/AutoGeneratedCode.cmake	Thu May 21 16:58:30 2015 +0200
@@ -26,7 +26,7 @@
     "${TARGET_BASE}.h"
     "${TARGET_BASE}.cpp"
     COMMAND 
-    python
+    ${PYTHON_EXECUTABLE}
     "${CMAKE_CURRENT_SOURCE_DIR}/Resources/EmbedResources.py"
     "${AUTOGENERATED_DIR}/EmbeddedResources"
     ${SCRIPT_ARGUMENTS}
--- a/Resources/CMake/BoostConfiguration.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/BoostConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -56,6 +56,7 @@
 
   if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
     list(APPEND BOOST_SOURCES
       ${BOOST_SOURCES_DIR}/libs/thread/src/pthread/once.cpp
@@ -76,9 +77,17 @@
       ${BOOST_SOURCES_DIR}/libs/thread/src/win32/tss_pe.cpp
       ${BOOST_FILESYSTEM_SOURCES_DIR}/windows_file_codecvt.cpp
       )
-    add_definitions(
-      -DBOOST_LOCALE_WITH_WCONV=1
-      )
+
+    # Starting with release 0.8.2, Orthanc statically links against
+    # libiconv, even on Windows. Indeed, the "WCONV" library of
+    # Windows XP seems not to support properly several codepages
+    # (notably "Latin3", "Hebrew", and "Arabic").
+
+    if (USE_BOOST_ICONV)
+      include(${ORTHANC_ROOT}/Resources/CMake/LibIconvConfiguration.cmake)
+    else()
+      add_definitions(-DBOOST_LOCALE_WITH_WCONV=1)
+    endif()
 
   else()
     message(FATAL_ERROR "Support your platform here")
@@ -90,6 +99,16 @@
       )
   endif()
 
+  if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
+    # This is a patch to compile Boost 1.55.0 with Clang 3.4 and later
+    # (including XCode 5.1). Fixes issue 14 of Orthanc.
+    # https://trac.macports.org/ticket/42282#comment:10
+    execute_process(
+      COMMAND patch -p0 -N -i ${ORTHANC_ROOT}/Resources/Patches/boost-1.55.0-clang-atomic.patch
+      WORKING_DIRECTORY ${BOOST_SOURCES_DIR}
+      )
+  endif()
+
   aux_source_directory(${BOOST_SOURCES_DIR}/libs/regex/src BOOST_REGEX_SOURCES)
 
   list(APPEND BOOST_SOURCES
--- a/Resources/CMake/Compiler.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/Compiler.cmake	Thu May 21 16:58:30 2015 +0200
@@ -28,28 +28,21 @@
     string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}")
   endforeach(flag_var)
 
+  # Add /Zm256 compiler option to Visual Studio to fix PCH errors
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zm256")
+
   add_definitions(
     -D_CRT_SECURE_NO_WARNINGS=1
     -D_CRT_SECURE_NO_DEPRECATE=1
     )
-  include_directories(${CMAKE_SOURCE_DIR}/Resources/ThirdParty/VisualStudio)
+  include_directories(${ORTHANC_ROOT}/Resources/ThirdParty/VisualStudio)
   link_libraries(netapi32)
 endif()
 
 
 if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
-    ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
-  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-    SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -I${LSB_PATH}/include")
-    SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward -fpermissive")
-    SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH}")
-  endif()
-
-  add_definitions(
-    -D_LARGEFILE64_SOURCE=1 
-    -D_FILE_OFFSET_BITS=64
-    )
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed")
+    ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
   set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined")
   set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
 
@@ -58,6 +51,15 @@
   set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "")
   link_libraries(uuid pthread rt)
 
+  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
+    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed")
+    add_definitions(
+      -D_LARGEFILE64_SOURCE=1 
+      -D_FILE_OFFSET_BITS=64
+      )
+    link_libraries(dl)
+  endif()
+
 elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
   add_definitions(
     -DWINVER=0x0501
@@ -79,6 +81,22 @@
 endif()
 
 
+if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
+  SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -I${LSB_PATH}/include")
+  SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward -fpermissive")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH}")
+endif()
+
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
+  # In FreeBSD, the "/usr/local/" folder contains the ports and need to be imported
+  SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I/usr/local/include")
+  SET(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -I/usr/local/include -I/usr/local/include/jsoncpp")
+  SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/usr/local/lib")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -L/usr/local/lib")
+endif()
+
+
 if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
   CHECK_INCLUDE_FILES(rpc.h HAVE_UUID_H)
 else()
@@ -89,6 +107,7 @@
   message(FATAL_ERROR "Please install the uuid-dev package")
 endif()
 
+
 if (${STATIC_BUILD})
   add_definitions(-DORTHANC_STATIC=1)
 else()
--- a/Resources/CMake/DcmtkConfiguration.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/DcmtkConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -3,6 +3,7 @@
   find_path(DCMTK_DICTIONARY_DIR_AUTO dicom.dic
     /usr/share/dcmtk
     /usr/share/libdcmtk2
+    /usr/local/share/dcmtk
     )
 
   message("Autodetected path to the DICOM dictionaries: ${DCMTK_DICTIONARY_DIR_AUTO}")
@@ -40,6 +41,10 @@
     set(HAVE_PROTOTYPE_GETSOCKNAME 1)
   endif()
 
+  set(DCMTK_PACKAGE_VERSION "3.6.0")
+  set(DCMTK_PACKAGE_VERSION_SUFFIX "")
+  set(DCMTK_PACKAGE_VERSION_NUMBER 360)
+
   CONFIGURE_FILE(
     ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
     ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
@@ -88,11 +93,18 @@
   AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_SOURCES)
   if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
     list(REMOVE_ITEM DCMTK_SOURCES 
       ${DCMTK_SOURCES_DIR}/oflog/libsrc/windebap.cc
       ${DCMTK_SOURCES_DIR}/oflog/libsrc/winsock.cc
       )
+    
+    execute_process(
+      COMMAND patch -p0 -N -i ${ORTHANC_ROOT}/Resources/Patches/dcmtk-linux-speed.patch
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      )
+
   elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
     list(REMOVE_ITEM DCMTK_SOURCES 
       ${DCMTK_SOURCES_DIR}/oflog/libsrc/unixsock.cc
@@ -101,7 +113,7 @@
     if (${CMAKE_COMPILER_IS_GNUCXX})
       # This is a patch for MinGW64
       execute_process(
-        COMMAND patch -p0 -i ${CMAKE_SOURCE_DIR}/Resources/Patches/dcmtk-mingw64.patch
+        COMMAND patch -p0 -N -i ${ORTHANC_ROOT}/Resources/Patches/dcmtk-mingw64.patch
         WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
         )
     endif()
@@ -114,6 +126,10 @@
     ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdictbi.cc
     )
 
+  #set_source_files_properties(${DCMTK_SOURCES}
+  #  PROPERTIES COMPILE_DEFINITIONS
+  #  "PACKAGE_VERSION=\"3.6.0\";PACKAGE_VERSION_NUMBER=\"360\"")
+
   # This fixes crashes related to the destruction of the DCMTK OFLogger
   # http://support.dcmtk.org/docs-snapshot/file_macros.html
   add_definitions(
@@ -154,7 +170,6 @@
   list(APPEND DCMTK_LIBRARIES "${tmp}")
 
   include_directories(${DCMTK_INCLUDE_DIR})
-  link_libraries(${DCMTK_LIBRARIES})
 
   add_definitions(
     -DHAVE_CONFIG_H=1
--- a/Resources/CMake/DownloadPackage.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/DownloadPackage.cmake	Thu May 21 16:58:30 2015 +0200
@@ -15,7 +15,12 @@
 ##
 
 if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
-  find_program(ZIP_EXECUTABLE 7z PATHS "$ENV{ProgramFiles}/7-Zip")
+  find_program(ZIP_EXECUTABLE 7z 
+    PATHS 
+    "$ENV{ProgramFiles}/7-Zip"
+    "$ENV{ProgramW6432}/7-Zip"
+    )
+
   if (${ZIP_EXECUTABLE} MATCHES "ZIP_EXECUTABLE-NOTFOUND")
     message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)")
   endif()
--- a/Resources/CMake/GoogleLogConfiguration.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/GoogleLogConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -30,6 +30,7 @@
 
   if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
     set(ac_cv_have_unistd_h 1)
     set(ac_cv_have_stdint_h 1)
@@ -65,44 +66,54 @@
   if (CMAKE_COMPILER_IS_GNUCXX)
     if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
       execute_process(
-        COMMAND patch utilities.cc ${CMAKE_SOURCE_DIR}/Resources/Patches/glog-utilities-lsb.diff
+        COMMAND patch -N utilities.cc ${ORTHANC_ROOT}/Resources/Patches/glog-utilities-lsb.diff
         WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src
         )
     else()
       execute_process(
-        COMMAND patch utilities.cc ${CMAKE_SOURCE_DIR}/Resources/Patches/glog-utilities.diff
+        COMMAND patch -N utilities.cc ${ORTHANC_ROOT}/Resources/Patches/glog-utilities.diff
         WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src
         )
     endif()
 
     execute_process(
-      COMMAND patch port.h ${CMAKE_SOURCE_DIR}/Resources/Patches/glog-port-h.diff 
+      COMMAND patch -N port.h ${ORTHANC_ROOT}/Resources/Patches/glog-port-h.diff 
       WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src/windows
       )
     execute_process(
-      COMMAND patch port.cc ${CMAKE_SOURCE_DIR}/Resources/Patches/glog-port-cc.diff 
+      COMMAND patch -N port.cc ${ORTHANC_ROOT}/Resources/Patches/glog-port-cc.diff 
       WORKING_DIRECTORY ${GOOGLE_LOG_SOURCES_DIR}/src/windows
       )
+
+  else(${MSVC})
+    # https://code.google.com/p/google-glog/issues/detail?id=117
+    configure_file(
+      ${ORTHANC_ROOT}/Resources/Patches/glog-visual-studio-port.h
+      ${GOOGLE_LOG_SOURCES_DIR}/src/windows/port.h
+      COPYONLY)
+
   endif()
 
+
   if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
     if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
       # Install the specific configuration for LSB SDK
       configure_file(
-        ${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleLogConfigurationLSB.h
+        ${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfigurationLSB.h
         ${GOOGLE_LOG_SOURCES_DIR}/src/config.h
         COPYONLY)
     elseif ("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")
       # Install the specific configuration for Mac OS
       configure_file(
-        ${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleLogConfigurationDarwin.h
+        ${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfigurationDarwin.h
         ${GOOGLE_LOG_SOURCES_DIR}/src/config.h
         COPYONLY)
     else()
       configure_file(
-        ${CMAKE_SOURCE_DIR}/Resources/CMake/GoogleLogConfiguration.h
+        ${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfiguration.h
         ${GOOGLE_LOG_SOURCES_DIR}/src/config.h
         COPYONLY)
     endif()
--- a/Resources/CMake/GoogleTestConfiguration.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/GoogleTestConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -8,10 +8,10 @@
   endif()
 
 elseif (STATIC_BUILD OR NOT USE_SYSTEM_GOOGLE_TEST)
-  SET(GTEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/gtest-1.6.0)
+  set(GTEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/gtest-1.7.0)
   DownloadPackage(
-    "4577b49f2973c90bf9ba69aa8166b786"
-    "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/gtest-1.6.0.zip"
+    "2d6ec8ccdf5c46b05ba54a9fd1d130d7"
+    "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/gtest-1.7.0.zip"
     "${GTEST_SOURCES_DIR}")
 
   include_directories(
@@ -23,6 +23,11 @@
     ${GTEST_SOURCES_DIR}/src/gtest-all.cc
     )
 
+  # https://code.google.com/p/googletest/issues/detail?id=412
+  if (MSVC) # VS2012 does not support tuples correctly yet
+    add_definitions(/D _VARIADIC_MAX=10)
+  endif()
+
 else()
   include(FindGTest)
   if (NOT GTEST_FOUND)
--- a/Resources/CMake/JsonCppConfiguration.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/JsonCppConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -23,7 +23,13 @@
     message(FATAL_ERROR "Please install the libjsoncpp-dev package")
   endif()
 
-  include_directories(/usr/include/jsoncpp)
+  find_path(JSONCPP_INCLUDE_DIR json/reader.h
+    /usr/include/jsoncpp
+    /usr/local/include/jsoncpp
+    )
+
+  message("JsonCpp include dir: ${JSONCPP_INCLUDE_DIR}")
+  include_directories(${JSONCPP_INCLUDE_DIR})
   link_libraries(jsoncpp)
 
 endif()
--- a/Resources/CMake/LibCurlConfiguration.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/LibCurlConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -26,7 +26,7 @@
     -DCURL_DISABLE_LDAP=1
     -DCURL_DISABLE_LDAPS=1
     -DCURL_DISABLE_POP3=1
-    -DCURL_DISABLE_PROXY=1
+    #-DCURL_DISABLE_PROXY=1
     -DCURL_DISABLE_RTSP=1
     -DCURL_DISABLE_TELNET=1
     -DCURL_DISABLE_TFTP=1
@@ -42,6 +42,7 @@
 
   if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
       ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
     if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
       SET(TMP_OS "x86_64")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/LibIconvConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,47 @@
+set(LIBICONV_SOURCES_DIR ${CMAKE_BINARY_DIR}/libiconv-1.14)
+DownloadPackage(
+  "e34509b1623cec449dfeb73d7ce9c6c6"
+  "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/libiconv-1.14.tar.gz"
+  "${LIBICONV_SOURCES_DIR}")
+
+# https://groups.google.com/d/msg/android-ndk/AS1nkxnk6m4/EQm09hD1tigJ
+add_definitions(
+  -DBOOST_LOCALE_WITH_ICONV=1
+  -DBUILDING_LIBICONV=1
+  -DIN_LIBRARY=1
+  -DLIBDIR=""
+  -DICONV_CONST=
+  )
+
+configure_file(
+  ${LIBICONV_SOURCES_DIR}/srclib/localcharset.h
+  ${LIBICONV_SOURCES_DIR}/include
+  COPYONLY)
+
+set(HAVE_VISIBILITY 0)
+set(ICONV_CONST ${ICONV_CONST})
+set(USE_MBSTATE_T 1)
+set(BROKEN_WCHAR_H 0)
+set(EILSEQ)
+set(HAVE_WCHAR_T 1)   
+configure_file(
+  ${LIBICONV_SOURCES_DIR}/include/iconv.h.build.in
+  ${LIBICONV_SOURCES_DIR}/include/iconv.h
+  )
+unset(HAVE_VISIBILITY)
+unset(ICONV_CONST)
+unset(USE_MBSTATE_T)
+unset(BROKEN_WCHAR_H)
+unset(EILSEQ)
+unset(HAVE_WCHAR_T)   
+
+include_directories(
+  ${LIBICONV_SOURCES_DIR}/include
+  )
+
+list(APPEND BOOST_SOURCES
+  ${LIBICONV_SOURCES_DIR}/lib/iconv.c  
+  ${LIBICONV_SOURCES_DIR}/lib/relocatable.c
+  ${LIBICONV_SOURCES_DIR}/libcharset/lib/localcharset.c  
+  ${LIBICONV_SOURCES_DIR}/libcharset/lib/relocatable.c
+  )
--- a/Resources/CMake/MongooseConfiguration.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/MongooseConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -1,13 +1,30 @@
 if (STATIC_BUILD OR NOT USE_SYSTEM_MONGOOSE)
   SET(MONGOOSE_SOURCES_DIR ${CMAKE_BINARY_DIR}/mongoose)
-  DownloadPackage(
-    "e718fc287b4eb1bd523be3fa00942bb0"
-    "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/mongoose-3.1.tgz"
-    "${MONGOOSE_SOURCES_DIR}")
+
+  if (0)
+    # Use Mongoose 3.1
+    DownloadPackage(
+      "e718fc287b4eb1bd523be3fa00942bb0"
+      "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/mongoose-3.1.tgz"
+      "${MONGOOSE_SOURCES_DIR}")
+    
+    add_definitions(-DMONGOOSE_USE_CALLBACKS=0)
+    set(MONGOOSE_PATCH ${ORTHANC_ROOT}/Resources/Patches/mongoose-3.1-patch.diff)
+
+  else() 
+    # Use Mongoose 3.8
+    DownloadPackage(
+      "7e3296295072792cdc3c633f9404e0c3"
+      "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/mongoose-3.8.tgz"
+      "${MONGOOSE_SOURCES_DIR}")
+    
+    add_definitions(-DMONGOOSE_USE_CALLBACKS=1)
+    set(MONGOOSE_PATCH ${ORTHANC_ROOT}/Resources/Patches/mongoose-3.8-patch.diff)
+  endif()
 
   # Patch mongoose
   execute_process(
-    COMMAND patch mongoose.c ${CMAKE_SOURCE_DIR}/Resources/Patches/mongoose-patch.diff
+    COMMAND patch -N mongoose.c ${MONGOOSE_PATCH}
     WORKING_DIRECTORY ${MONGOOSE_SOURCES_DIR}
     )
 
@@ -56,5 +73,13 @@
     message(FATAL_ERROR "Please install the mongoose-devel package")
   endif()
 
+  if (SYSTEM_MONGOOSE_USE_CALLBACKS)
+    add_definitions(-DMONGOOSE_USE_CALLBACKS=1)
+  else()
+    add_definitions(-DMONGOOSE_USE_CALLBACKS=0)
+  endif()
+
   link_libraries(mongoose)
 endif()
+
+
--- a/Resources/CMake/OpenSslConfiguration.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/OpenSslConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -188,7 +188,7 @@
 
   elseif ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
     execute_process(
-      COMMAND patch ui_openssl.c ${CMAKE_SOURCE_DIR}/Resources/Patches/openssl-lsb.diff
+      COMMAND patch -N ui_openssl.c ${ORTHANC_ROOT}/Resources/Patches/openssl-lsb.diff
       WORKING_DIRECTORY ${OPENSSL_SOURCES_DIR}/crypto/ui
       )
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CMake/PugixmlConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,35 @@
+if (USE_PUGIXML)
+  add_definitions(-DORTHANC_PUGIXML_ENABLED=1)
+
+  if (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML)
+    set(PUGIXML_SOURCES_DIR ${CMAKE_BINARY_DIR}/pugixml-1.4)
+
+    DownloadPackage(
+      "7c56c91cfe3ecdee248a8e4892ef5781"
+      "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/pugixml-1.4.tar.gz"
+      "${PUGIXML_SOURCES_DIR}")
+
+    include_directories(
+      ${PUGIXML_SOURCES_DIR}/src
+      )
+
+    set(PUGIXML_SOURCES
+      ${PUGIXML_SOURCES_DIR}/src/vlog_is_on.cc
+      )
+
+    list(APPEND THIRD_PARTY_SOURCES 
+      ${PUGIXML_SOURCES_DIR}/src/pugixml.cpp
+      )
+
+  else()
+    CHECK_INCLUDE_FILE_CXX(pugixml.hpp HAVE_PUGIXML_H)
+    if (NOT HAVE_PUGIXML_H)
+      message(FATAL_ERROR "Please install the libpugixml-dev package")
+    endif()
+
+    link_libraries(pugixml)
+  endif()
+
+else()
+  add_definitions(-DORTHANC_PUGIXML_ENABLED=0)
+endif()
--- a/Resources/CMake/SQLiteConfiguration.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/SQLiteConfiguration.cmake	Thu May 21 16:58:30 2015 +0200
@@ -28,8 +28,14 @@
     message(FATAL_ERROR "Please install the libsqlite3-dev package")
   endif()
 
+  find_path(SQLITE_INCLUDE_DIR sqlite3.h
+    /usr/include
+    /usr/local/include
+    )
+  message("SQLite include dir: ${SQLITE_INCLUDE_DIR}")
+
   # Autodetection of the version of SQLite
-  file(STRINGS "/usr/include/sqlite3.h" SQLITE_VERSION_NUMBER1 REGEX "#define SQLITE_VERSION_NUMBER.*$")    
+  file(STRINGS "${SQLITE_INCLUDE_DIR}/sqlite3.h" SQLITE_VERSION_NUMBER1 REGEX "#define SQLITE_VERSION_NUMBER.*$")    
   string(REGEX REPLACE "#define SQLITE_VERSION_NUMBER(.*)$" "\\1" SQLITE_VERSION_NUMBER ${SQLITE_VERSION_NUMBER1})
 
   message("Detected version of SQLite: ${SQLITE_VERSION_NUMBER}")
--- a/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Thu May 21 16:58:30 2015 +0200
@@ -1,6 +1,6 @@
 macro(ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS PrecompiledHeaders PrecompiledSource Sources)
   get_filename_component(PrecompiledBasename ${PrecompiledHeaders} NAME_WE)
-  set(PrecompiledBinary "${CMAKE_CURRENT_BINARY_DIR}/${PrecompiledBasename}.pch")
+  set(PrecompiledBinary "$(IntDir)/${PrecompiledBasename}.pch")
 
   set_source_files_properties(${PrecompiledSource}
     PROPERTIES COMPILE_FLAGS "/Yc\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\""
--- a/Resources/Configuration.json	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Configuration.json	Thu May 21 16:58:30 2015 +0200
@@ -28,11 +28,18 @@
   // of patients)
   "MaximumPatientCount" : 0,
   
-  // List of paths to the custom Lua scripts to load into this
-  // instance of Orthanc
+  // List of paths to the custom Lua scripts that are to be loaded
+  // into this instance of Orthanc
   "LuaScripts" : [
   ],
 
+  // List of paths to the plugins that are to be loaded into this
+  // instance of Orthanc (e.g. "./libPluginTest.so" for Linux, or
+  // "./PluginTest.dll" for Windows). These paths can refer to
+  // folders, in which case they will be scanned non-recursively to
+  // find shared libraries.
+  "Plugins" : [
+  ],
 
 
   /**
@@ -57,6 +64,21 @@
   // The DICOM port
   "DicomPort" : 4242,
 
+  // The default encoding that is assumed for DICOM files without
+  // "SpecificCharacterSet" DICOM tag. The allowed values are "Ascii",
+  // "Utf8", "Latin1", "Latin2", "Latin3", "Latin4", "Latin5",
+  // "Cyrillic", "Windows1251", "Arabic", "Greek", "Hebrew", "Thai",
+  // "Japanese", and "Chinese".
+  "DefaultEncoding" : "Latin1",
+
+  // The transfer syntaxes that are accepted by Orthanc C-Store SCP
+  "DeflatedTransferSyntaxAccepted"     : true,
+  "JpegTransferSyntaxAccepted"         : true,
+  "Jpeg2000TransferSyntaxAccepted"     : true,
+  "JpegLosslessTransferSyntaxAccepted" : true,
+  "JpipTransferSyntaxAccepted"         : true,
+  "Mpeg2TransferSyntaxAccepted"        : true,
+  "RleTransferSyntaxAccepted"          : true,
 
 
   /**
@@ -100,8 +122,9 @@
     /**
      * A fourth parameter is available to enable patches for a
      * specific PACS manufacturer. The allowed values are currently
-     * "Generic" (default value), "ClearCanvas", "MedInria" and
-     * "Dcm4Chee". This parameter is case-sensitive.
+     * "Generic" (default value), "StoreScp" (storescp tool from
+     * DCMTK), "ClearCanvas", "MedInria" and "Dcm4Chee". This
+     * parameter is case-sensitive.
      **/
     // "clearcanvas" : [ "CLEARCANVAS", "192.168.1.1", 104, "ClearCanvas" ]
   },
@@ -117,6 +140,11 @@
     // "peer2" : [ "http://localhost:8044/" ]
   },
 
+  // 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"
+  //   "HttpProxy" : "proxyUser:proxyPassword@192.168.0.1:3128"
+  "HttpProxy" : "",
 
 
   /**
@@ -169,5 +197,30 @@
 
   // The maximum number of results for a single C-FIND request at the
   // Instance level. Setting this option to "0" means no limit.
-  "LimitFindInstances" : 0
+  "LimitFindInstances" : 0,
+
+  // The maximum number of active jobs in the Orthanc scheduler. When
+  // this limit is reached, the addition of new jobs is blocked until
+  // some job finishes.
+  "LimitJobs" : 10,
+
+  // If this option is set to "false", Orthanc will not log the
+  // resources that are exported to other DICOM modalities of Orthanc
+  // peers in the URI "/exports". This is useful to prevent the index
+  // to grow indefinitely in auto-routing tasks.
+  "LogExportedResources" : true,
+
+  // Enable or disable HTTP Keep-Alive (deprecated). Set this option
+  // to "true" only in the case of high HTTP loads.
+  "KeepAlive" : false,
+
+  // If this option is set to "false", Orthanc will run in index-only
+  // mode. The DICOM files will not be stored on the drive.
+  "StoreDicom" : true,
+
+  // DICOM associations are kept open as long as new DICOM commands
+  // are issued. This option sets the number of seconds of inactivity
+  // to wait before automatically closing a DICOM association. If set
+  // to 0, the connection is closed immediately.
+  "DicomAssociationCloseDelay" : 5
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/DicomConformanceStatement.py	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,31 @@
+#!/usr/bin/python
+
+# This file injects the UID information into the DICOM conformance
+# statement of Orthanc
+
+import re
+
+# Read the conformance statement of Orthanc
+with open('DicomConformanceStatement.txt', 'r') as f:
+    statements = f.readlines()
+
+# Create an index of all the DICOM UIDs that are known to DCMTK
+uids = {}
+with open('/usr/include/dcmtk/dcmdata/dcuid.h', 'r') as dcmtk:
+    for l in dcmtk.readlines():
+        m = re.match(r'#define UID_(.+?)\s*"(.+?)"', l)
+        if m != None:
+            uids[m.group(1)] = m.group(2)
+
+# Loop over the lines of the statement, looking for the "|" separator
+with open('/tmp/DicomConformanceStatement.txt', 'w') as f:
+    for l in statements:
+        m = re.match(r'(\s*)(.*?)(\s*)\|.*$', l)
+        if m != None:
+            name = m.group(2)
+            uid = uids[name]
+            f.write('%s%s%s| %s\n' % (m.group(1), name, m.group(3), uid))
+
+        else:
+            # No "|" in this line, just output it
+            f.write(l)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/DicomConformanceStatement.txt	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,254 @@
+======================================
+DICOM Conformance Statement of Orthanc
+======================================
+
+
+---------------------
+Echo SCP Conformance
+---------------------
+
+Orthanc supports the following SOP Classes as an SCP for C-Echo:
+
+  VerificationSOPClass      | 1.2.840.10008.1.1
+
+
+---------------------
+Store SCP Conformance
+---------------------
+
+Orthanc supports the following SOP Classes as an SCP for C-Store:
+
+  AmbulatoryECGWaveformStorage                             | 1.2.840.10008.5.1.4.1.1.9.1.3
+  ArterialPulseWaveformStorage                             | 1.2.840.10008.5.1.4.1.1.9.5.1
+  AutorefractionMeasurementsStorage                        | 1.2.840.10008.5.1.4.1.1.78.2
+  BasicStructuredDisplayStorage                            | 1.2.840.10008.5.1.4.1.1.131
+  BasicTextSRStorage                                       | 1.2.840.10008.5.1.4.1.1.88.11
+  BasicVoiceAudioWaveformStorage                           | 1.2.840.10008.5.1.4.1.1.9.4.1
+  BlendingSoftcopyPresentationStateStorage                 | 1.2.840.10008.5.1.4.1.1.11.4
+  BreastTomosynthesisImageStorage                          | 1.2.840.10008.5.1.4.1.1.13.1.3
+  CardiacElectrophysiologyWaveformStorage                  | 1.2.840.10008.5.1.4.1.1.9.3.1
+  ChestCADSRStorage                                        | 1.2.840.10008.5.1.4.1.1.88.65
+  ColonCADSRStorage                                        | 1.2.840.10008.5.1.4.1.1.88.69
+  ColorSoftcopyPresentationStateStorage                    | 1.2.840.10008.5.1.4.1.1.11.2
+  ComprehensiveSRStorage                                   | 1.2.840.10008.5.1.4.1.1.88.33
+  ComputedRadiographyImageStorage                          | 1.2.840.10008.5.1.4.1.1.1
+  CTImageStorage                                           | 1.2.840.10008.5.1.4.1.1.2
+  DeformableSpatialRegistrationStorage                     | 1.2.840.10008.5.1.4.1.1.66.3
+  DigitalIntraOralXRayImageStorageForPresentation          | 1.2.840.10008.5.1.4.1.1.1.3
+  DigitalIntraOralXRayImageStorageForProcessing            | 1.2.840.10008.5.1.4.1.1.1.3.1
+  DigitalMammographyXRayImageStorageForPresentation        | 1.2.840.10008.5.1.4.1.1.1.2
+  DigitalMammographyXRayImageStorageForProcessing          | 1.2.840.10008.5.1.4.1.1.1.2.1
+  DigitalXRayImageStorageForPresentation                   | 1.2.840.10008.5.1.4.1.1.1.1
+  DigitalXRayImageStorageForProcessing                     | 1.2.840.10008.5.1.4.1.1.1.1.1
+  EncapsulatedCDAStorage                                   | 1.2.840.10008.5.1.4.1.1.104.2
+  EncapsulatedPDFStorage                                   | 1.2.840.10008.5.1.4.1.1.104.1
+  EnhancedCTImageStorage                                   | 1.2.840.10008.5.1.4.1.1.2.1
+  EnhancedMRColorImageStorage                              | 1.2.840.10008.5.1.4.1.1.4.3
+  EnhancedMRImageStorage                                   | 1.2.840.10008.5.1.4.1.1.4.1
+  EnhancedPETImageStorage                                  | 1.2.840.10008.5.1.4.1.1.130
+  EnhancedSRStorage                                        | 1.2.840.10008.5.1.4.1.1.88.22
+  EnhancedUSVolumeStorage                                  | 1.2.840.10008.5.1.4.1.1.6.2
+  EnhancedXAImageStorage                                   | 1.2.840.10008.5.1.4.1.1.12.1.1
+  EnhancedXRFImageStorage                                  | 1.2.840.10008.5.1.4.1.1.12.2.1
+  GeneralAudioWaveformStorage                              | 1.2.840.10008.5.1.4.1.1.9.4.2
+  GeneralECGWaveformStorage                                | 1.2.840.10008.5.1.4.1.1.9.1.2
+  GenericImplantTemplateStorage                            | 1.2.840.10008.5.1.4.43.1
+  GrayscaleSoftcopyPresentationStateStorage                | 1.2.840.10008.5.1.4.1.1.11.1
+  HemodynamicWaveformStorage                               | 1.2.840.10008.5.1.4.1.1.9.2.1
+  ImplantAssemblyTemplateStorage                           | 1.2.840.10008.5.1.4.44.1
+  ImplantationPlanSRDocumentStorage                        | 1.2.840.10008.5.1.4.1.1.88.70
+  ImplantTemplateGroupStorage                              | 1.2.840.10008.5.1.4.45.1
+  IntraocularLensCalculationsStorage                       | 1.2.840.10008.5.1.4.1.1.78.8
+  KeratometryMeasurementsStorage                           | 1.2.840.10008.5.1.4.1.1.78.3
+  KeyObjectSelectionDocumentStorage                        | 1.2.840.10008.5.1.4.1.1.88.59
+  LensometryMeasurementsStorage                            | 1.2.840.10008.5.1.4.1.1.78.1
+  MacularGridThicknessAndVolumeReportStorage               | 1.2.840.10008.5.1.4.1.1.79.1
+  MammographyCADSRStorage                                  | 1.2.840.10008.5.1.4.1.1.88.50
+  MRImageStorage                                           | 1.2.840.10008.5.1.4.1.1.4
+  MRSpectroscopyStorage                                    | 1.2.840.10008.5.1.4.1.1.4.2
+  MultiframeGrayscaleByteSecondaryCaptureImageStorage      | 1.2.840.10008.5.1.4.1.1.7.2
+  MultiframeGrayscaleWordSecondaryCaptureImageStorage      | 1.2.840.10008.5.1.4.1.1.7.3
+  MultiframeSingleBitSecondaryCaptureImageStorage          | 1.2.840.10008.5.1.4.1.1.7.1
+  MultiframeTrueColorSecondaryCaptureImageStorage          | 1.2.840.10008.5.1.4.1.1.7.4
+  NuclearMedicineImageStorage                              | 1.2.840.10008.5.1.4.1.1.20
+  OphthalmicAxialMeasurementsStorage                       | 1.2.840.10008.5.1.4.1.1.78.7
+  OphthalmicPhotography16BitImageStorage                   | 1.2.840.10008.5.1.4.1.1.77.1.5.2
+  OphthalmicPhotography8BitImageStorage                    | 1.2.840.10008.5.1.4.1.1.77.1.5.1
+  OphthalmicTomographyImageStorage                         | 1.2.840.10008.5.1.4.1.1.77.1.5.4
+  OphthalmicVisualFieldStaticPerimetryMeasurementsStorage  | 1.2.840.10008.5.1.4.1.1.80.1
+  PositronEmissionTomographyImageStorage                   | 1.2.840.10008.5.1.4.1.1.128
+  ProcedureLogStorage                                      | 1.2.840.10008.5.1.4.1.1.88.40
+  PseudoColorSoftcopyPresentationStateStorage              | 1.2.840.10008.5.1.4.1.1.11.3
+  RawDataStorage                                           | 1.2.840.10008.5.1.4.1.1.66
+  RealWorldValueMappingStorage                             | 1.2.840.10008.5.1.4.1.1.67
+  RespiratoryWaveformStorage                               | 1.2.840.10008.5.1.4.1.1.9.6.1
+  RTBeamsTreatmentRecordStorage                            | 1.2.840.10008.5.1.4.1.1.481.4
+  RTBrachyTreatmentRecordStorage                           | 1.2.840.10008.5.1.4.1.1.481.6
+  RTDoseStorage                                            | 1.2.840.10008.5.1.4.1.1.481.2
+  RTImageStorage                                           | 1.2.840.10008.5.1.4.1.1.481.1
+  RTIonBeamsTreatmentRecordStorage                         | 1.2.840.10008.5.1.4.1.1.481.9
+  RTIonPlanStorage                                         | 1.2.840.10008.5.1.4.1.1.481.8
+  RTPlanStorage                                            | 1.2.840.10008.5.1.4.1.1.481.5
+  RTStructureSetStorage                                    | 1.2.840.10008.5.1.4.1.1.481.3
+  RTTreatmentSummaryRecordStorage                          | 1.2.840.10008.5.1.4.1.1.481.7
+  SecondaryCaptureImageStorage                             | 1.2.840.10008.5.1.4.1.1.7
+  SegmentationStorage                                      | 1.2.840.10008.5.1.4.1.1.66.4
+  SpatialFiducialsStorage                                  | 1.2.840.10008.5.1.4.1.1.66.2
+  SpatialRegistrationStorage                               | 1.2.840.10008.5.1.4.1.1.66.1
+  SpectaclePrescriptionReportStorage                       | 1.2.840.10008.5.1.4.1.1.78.6
+  StereometricRelationshipStorage                          | 1.2.840.10008.5.1.4.1.1.77.1.5.3
+  SubjectiveRefractionMeasurementsStorage                  | 1.2.840.10008.5.1.4.1.1.78.4
+  SurfaceSegmentationStorage                               | 1.2.840.10008.5.1.4.1.1.66.5
+  TwelveLeadECGWaveformStorage                             | 1.2.840.10008.5.1.4.1.1.9.1.1
+  UltrasoundImageStorage                                   | 1.2.840.10008.5.1.4.1.1.6.1
+  UltrasoundMultiframeImageStorage                         | 1.2.840.10008.5.1.4.1.1.3.1
+  VideoEndoscopicImageStorage                              | 1.2.840.10008.5.1.4.1.1.77.1.1.1
+  VideoMicroscopicImageStorage                             | 1.2.840.10008.5.1.4.1.1.77.1.2.1
+  VideoPhotographicImageStorage                            | 1.2.840.10008.5.1.4.1.1.77.1.4.1
+  VisualAcuityMeasurementsStorage                          | 1.2.840.10008.5.1.4.1.1.78.5
+  VLEndoscopicImageStorage                                 | 1.2.840.10008.5.1.4.1.1.77.1.1
+  VLMicroscopicImageStorage                                | 1.2.840.10008.5.1.4.1.1.77.1.2
+  VLPhotographicImageStorage                               | 1.2.840.10008.5.1.4.1.1.77.1.4
+  VLSlideCoordinatesMicroscopicImageStorage                | 1.2.840.10008.5.1.4.1.1.77.1.3
+  VLWholeSlideMicroscopyImageStorage                       | 1.2.840.10008.5.1.4.1.1.77.1.6
+  XAXRFGrayscaleSoftcopyPresentationStateStorage           | 1.2.840.10008.5.1.4.1.1.11.5
+  XRay3DAngiographicImageStorage                           | 1.2.840.10008.5.1.4.1.1.13.1.1
+  XRay3DCraniofacialImageStorage                           | 1.2.840.10008.5.1.4.1.1.13.1.2
+  XRayAngiographicImageStorage                             | 1.2.840.10008.5.1.4.1.1.12.1
+  XRayRadiationDoseSRStorage                               | 1.2.840.10008.5.1.4.1.1.88.67
+  XRayRadiofluoroscopicImageStorage                        | 1.2.840.10008.5.1.4.1.1.12.2
+
+  RETIRED_HardcopyColorImageStorage                        | 1.2.840.10008.5.1.1.30
+  RETIRED_HardcopyGrayscaleImageStorage                    | 1.2.840.10008.5.1.1.29
+  RETIRED_NuclearMedicineImageStorage                      | 1.2.840.10008.5.1.4.1.1.5
+  RETIRED_StandaloneCurveStorage                           | 1.2.840.10008.5.1.4.1.1.9
+  RETIRED_StandaloneModalityLUTStorage                     | 1.2.840.10008.5.1.4.1.1.10
+  RETIRED_StandaloneOverlayStorage                         | 1.2.840.10008.5.1.4.1.1.8
+  RETIRED_StandalonePETCurveStorage                        | 1.2.840.10008.5.1.4.1.1.129
+  RETIRED_StandaloneVOILUTStorage                          | 1.2.840.10008.5.1.4.1.1.11
+  RETIRED_StoredPrintStorage                               | 1.2.840.10008.5.1.1.27
+  RETIRED_UltrasoundImageStorage                           | 1.2.840.10008.5.1.4.1.1.6
+  RETIRED_UltrasoundMultiframeImageStorage                 | 1.2.840.10008.5.1.4.1.1.3
+  RETIRED_VLImageStorage                                   | 1.2.840.10008.5.1.4.1.1.77.1
+  RETIRED_VLMultiFrameImageStorage                         | 1.2.840.10008.5.1.4.1.1.77.2
+  RETIRED_XRayAngiographicBiPlaneImageStorage              | 1.2.840.10008.5.1.4.1.1.12.3
+
+  DRAFT_SRAudioStorage                                     | 1.2.840.10008.5.1.4.1.1.88.2
+  DRAFT_SRComprehensiveStorage                             | 1.2.840.10008.5.1.4.1.1.88.4
+  DRAFT_SRDetailStorage                                    | 1.2.840.10008.5.1.4.1.1.88.3
+  DRAFT_SRTextStorage                                      | 1.2.840.10008.5.1.4.1.1.88.1
+  DRAFT_WaveformStorage                                    | 1.2.840.10008.5.1.4.1.1.9.1
+  DRAFT_RTBeamsDeliveryInstructionStorage                  | 1.2.840.10008.5.1.4.34.1
+
+
+--------------------
+Find SCP Conformance
+--------------------
+
+Orthanc supports the following SOP Classes as an SCP for C-Find:
+
+  FINDPatientRootQueryRetrieveInformationModel   | 1.2.840.10008.5.1.4.1.2.1.1
+  FINDStudyRootQueryRetrieveInformationModel     | 1.2.840.10008.5.1.4.1.2.2.1
+
+
+--------------------
+Move SCP Conformance
+--------------------
+
+Orthanc supports the following SOP Classes as an SCP for C-Move:
+
+  MOVEPatientRootQueryRetrieveInformationModel   | 1.2.840.10008.5.1.4.1.2.1.2
+  MOVEStudyRootQueryRetrieveInformationModel     | 1.2.840.10008.5.1.4.1.2.2.2
+
+
+---------------------
+Echo SCU Conformance
+---------------------
+
+Orthanc supports the following SOP Classes as an SCU for C-Echo:
+
+  VerificationSOPClass      | 1.2.840.10008.1.1
+
+
+---------------------
+Store SCU Conformance
+---------------------
+
+All the SOP Classes that are listed in the "Store SCP Conformance"
+(see above) section are available as an SCU for C-Store.
+
+
+--------------------
+Find SCU Conformance
+--------------------
+
+Orthanc supports the following SOP Classes as an SCU for C-Find:
+
+  FINDPatientRootQueryRetrieveInformationModel  | 1.2.840.10008.5.1.4.1.2.1.1
+  FINDStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.1
+  FINDStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.1
+
+
+-----------------
+Transfer Syntaxes
+-----------------
+
+Orthanc will accept and negociate presentation contexts for all of the
+abovementioned supported SOP Classes using any of the following
+transfer syntaxes:
+
+  LittleEndianImplicitTransferSyntax                                    | 1.2.840.10008.1.2
+  LittleEndianExplicitTransferSyntax                                    | 1.2.840.10008.1.2.1
+  BigEndianExplicitTransferSyntax                                       | 1.2.840.10008.1.2.2
+  DeflatedExplicitVRLittleEndianTransferSyntax                          | 1.2.840.10008.1.2.1.99
+  JPEGProcess1TransferSyntax                                            | 1.2.840.10008.1.2.4.50
+  JPEGProcess2_4TransferSyntax                                          | 1.2.840.10008.1.2.4.51
+  JPEGProcess3_5TransferSyntax                                          | 1.2.840.10008.1.2.4.52
+  JPEGProcess6_8TransferSyntax                                          | 1.2.840.10008.1.2.4.53
+  JPEGProcess7_9TransferSyntax                                          | 1.2.840.10008.1.2.4.54
+  JPEGProcess10_12TransferSyntax                                        | 1.2.840.10008.1.2.4.55
+  JPEGProcess11_13TransferSyntax                                        | 1.2.840.10008.1.2.4.56
+  JPEGProcess14TransferSyntax                                           | 1.2.840.10008.1.2.4.57
+  JPEGProcess15TransferSyntax                                           | 1.2.840.10008.1.2.4.58
+  JPEGProcess16_18TransferSyntax                                        | 1.2.840.10008.1.2.4.59
+  JPEGProcess17_19TransferSyntax                                        | 1.2.840.10008.1.2.4.60
+  JPEGProcess20_22TransferSyntax                                        | 1.2.840.10008.1.2.4.61
+  JPEGProcess21_23TransferSyntax                                        | 1.2.840.10008.1.2.4.62
+  JPEGProcess24_26TransferSyntax                                        | 1.2.840.10008.1.2.4.63
+  JPEGProcess25_27TransferSyntax                                        | 1.2.840.10008.1.2.4.64
+  JPEGProcess28TransferSyntax                                           | 1.2.840.10008.1.2.4.65
+  JPEGProcess29TransferSyntax                                           | 1.2.840.10008.1.2.4.66
+  JPEGProcess14SV1TransferSyntax                                        | 1.2.840.10008.1.2.4.70
+  JPEGLSLosslessTransferSyntax                                          | 1.2.840.10008.1.2.4.80
+  JPEGLSLossyTransferSyntax                                             | 1.2.840.10008.1.2.4.81
+  JPEG2000LosslessOnlyTransferSyntax                                    | 1.2.840.10008.1.2.4.90
+  JPEG2000TransferSyntax                                                | 1.2.840.10008.1.2.4.91
+  JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax | 1.2.840.10008.1.2.4.92
+  JPEG2000Part2MulticomponentImageCompressionTransferSyntax             | 1.2.840.10008.1.2.4.93
+  JPIPReferencedTransferSyntax                                          | 1.2.840.10008.1.2.4.94
+  JPIPReferencedDeflateTransferSyntax                                   | 1.2.840.10008.1.2.4.95
+  MPEG2MainProfileAtMainLevelTransferSyntax                             | 1.2.840.10008.1.2.4.100
+  MPEG2MainProfileAtHighLevelTransferSyntax                             | 1.2.840.10008.1.2.4.101
+  RLELosslessTransferSyntax                                             | 1.2.840.10008.1.2.5
+
+It is possible to disable a subset of these transfer syntaxes thanks to the
+"*TransferSyntaxAccepted" options in the Orthanc configuration file.
+
+When possible, Orthanc will prefer the
+LittleEndianImplicitTransferSyntax transfer syntax
+(1.2.840.10008.1.2).
+
+Orthanc does not support extended negotiation.
+
+
+--------------------
+Implementation notes
+--------------------
+
+The information above about the SCP support is readily extracted from
+the function "Orthanc::Internals::AcceptAssociation()" from file
+"OrthancServer/Internals/CommandDispatcher.cpp".
+
+The information above about the SCU support is derived from the class
+"Orthanc::DicomUserConnection" from file
+"OrthancServer/DicomProtocol/DicomUserConnection.cpp".
--- a/Resources/EmbedResources.py	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/EmbedResources.py	Thu May 21 16:58:30 2015 +0200
@@ -1,6 +1,6 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
-# Belgium
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -86,6 +86,13 @@
         content = {}
         for root, dirs, files in os.walk(pathName):
             base = os.path.relpath(root, pathName)
+
+            # Fix issue #24 (Build fails on OSX when directory has .DS_Store files):
+            # Ignore folders whose name starts with a dot (".")
+            if base.find('/.') != -1:
+                print('Ignoring folder: %s' % root)
+                continue
+
             for f in files:
                 if f.find('~') == -1:  # Ignore Emacs backup files
                     if base == '.':
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/EncodingTests.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,61 @@
+static const unsigned int testEncodingsCount = 18;
+static const ::Orthanc::Encoding testEncodings[] = {
+  ::Orthanc::Encoding_Latin5,
+  ::Orthanc::Encoding_Hebrew,
+  ::Orthanc::Encoding_Greek,
+  ::Orthanc::Encoding_Arabic,
+  ::Orthanc::Encoding_Cyrillic,
+  ::Orthanc::Encoding_Latin4,
+  ::Orthanc::Encoding_Latin3,
+  ::Orthanc::Encoding_Latin2,
+  ::Orthanc::Encoding_Latin1,
+  ::Orthanc::Encoding_Utf8,
+  ::Orthanc::Encoding_Thai,
+  ::Orthanc::Encoding_Japanese,
+  ::Orthanc::Encoding_Ascii,
+  ::Orthanc::Encoding_Windows1251,
+  ::Orthanc::Encoding_Chinese,
+  ::Orthanc::Encoding_Windows1251,
+  ::Orthanc::Encoding_Windows1251,
+  ::Orthanc::Encoding_Windows1251
+};
+static const char *testEncodingsEncoded[18] = {
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2\xdd",
+  "\x54\x65\x73\x74\xe3",
+  "\x54\x65\x73\x74\xc8",
+  "\x54\x65\x73\x74\xd5",
+  "\x54\x65\x73\x74\xb4\xfb",
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf3",
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2\xf8\xa9",
+  "\x54\x65\x73\x74\xe9\xe4\xf6",
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xd0\x94\xce\x98\xc4\x9d\xd7\x93\xd8\xb5\xc4\xb7\xd1\x9b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0",
+  "\x54\x65\x73\x74\xfb",
+  "\x54\x65\x73\x74\x84\x44\x83\xa6\xc8",
+  "\x54\x65\x73\x74",
+  "\x54\x65\x73\x74\xc4\x9e",
+  "\x81\x30\x89\x37\x81\x30\x89\x38\xA8\xA4\xA8\xA2\x81\x30\x89\x39\x81\x30\x8A\x30",
+  "\xd0\xe5\xed\xf2\xe3\xe5\xed\xee\xe3\xf0\xe0\xf4\xe8\xff",
+  "\xD2\xE0\xE7",
+  "\xcf\xf0\xff\xec\xe0\xff"
+};
+static const char *testEncodingsExpected[18] = {
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xc4\xb0",
+  "\x54\x65\x73\x74\xd7\x93",
+  "\x54\x65\x73\x74\xce\x98",
+  "\x54\x65\x73\x74\xd8\xb5",
+  "\x54\x65\x73\x74\xd0\x94\xd1\x9b",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc4\xb7",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xc4\x9d\xc4\xb0",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xd0\x94\xce\x98\xc4\x9d\xd7\x93\xd8\xb5\xc4\xb7\xd1\x9b\xe0\xb9\x9b\xef\xbe\x88\xc4\xb0",
+  "\x54\x65\x73\x74\xe0\xb9\x9b",
+  "\x54\x65\x73\x74\xd0\x94\xce\x98\xef\xbe\x88",
+  "\x54\x65\x73\x74",
+  "\x54\x65\x73\x74\xd0\x94\xd1\x9b",
+  "\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3",
+  "\xd0\xa0\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb3\xd0\xb5\xd0\xbd\xd0\xbe\xd0\xb3\xd1\x80\xd0\xb0\xd1\x84\xd0\xb8\xd1\x8f",
+  "\xd0\xa2\xd0\xb0\xd0\xb7",
+  "\xd0\x9f\xd1\x80\xd1\x8f\xd0\xbc\xd0\xb0\xd1\x8f"
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/EncodingTests.py	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,74 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+source = u'TestéäöòДΘĝדصķћ๛ネİ'
+
+encodings = {
+    'UTF-8' : 'Utf8',
+    'ASCII' : 'Ascii',
+    'ISO-8859-1' : 'Latin1',
+    'ISO-8859-2' : 'Latin2',
+    'ISO-8859-3' : 'Latin3',
+    'ISO-8859-4' : 'Latin4',
+    'ISO-8859-9' : 'Latin5',
+    'ISO-8859-5' : 'Cyrillic',
+    'WINDOWS-1251' : 'Windows1251',
+    'ISO-8859-6' : 'Arabic',
+    'ISO-8859-7' : 'Greek',
+    'ISO-8859-8' : 'Hebrew',
+    'TIS-620' : 'Thai',
+    'SHIFT-JIS' : 'Japanese',
+    #'GB18030' : 'Chinese',  # Done manually below (*)
+}
+
+#from encodings.aliases import aliases
+#for a, b in aliases.iteritems():
+#    print '%s : %s' % (a, b)
+
+
+# "63" corresponds to "?"
+l = []
+encoded = []
+expected = []
+
+def ToArray(source):
+    result = ''
+    for byte in bytearray(source):
+        result += '\\x%02x' % byte
+    return '"%s"' % result
+    
+
+for encoding, orthancEnumeration in encodings.iteritems():
+    l.append('::Orthanc::Encoding_%s' % orthancEnumeration)
+    s = source.encode(encoding, 'ignore')
+    encoded.append(ToArray(s))
+    expected.append(ToArray(s.decode(encoding).encode('utf-8')))
+
+
+# https://en.wikipedia.org/wiki/GB_18030#Technical_details  (*)
+l.append('::Orthanc::Encoding_Chinese')
+expected.append(ToArray('Þßàáâã'))
+encoded.append('"\\x81\\x30\\x89\\x37\\x81\\x30\\x89\\x38\\xA8\\xA4\\xA8\\xA2\\x81\\x30\\x89\\x39\\x81\\x30\\x8A\\x30"')
+
+# Issue 32
+# "encoded" is the copy/paste from "dcm2xml +Ca cyrillic Issue32.dcm"
+l.append('::Orthanc::Encoding_Windows1251')
+encoded.append('"\\xd0\\xe5\\xed\\xf2\\xe3\\xe5\\xed\\xee\\xe3\\xf0\\xe0\\xf4\\xe8\\xff"')
+expected.append(ToArray('Рентгенография'))
+l.append('::Orthanc::Encoding_Windows1251')
+encoded.append('"\\xD2\\xE0\\xE7"')
+expected.append(ToArray('Таз'))
+l.append('::Orthanc::Encoding_Windows1251')
+encoded.append('"\\xcf\\xf0\\xff\\xec\\xe0\\xff"')
+expected.append(ToArray('Прямая'))
+
+
+if True:
+    print 'static const unsigned int testEncodingsCount = %d;' % len(l)
+    print 'static const ::Orthanc::Encoding testEncodings[] = {\n  %s\n};' % (',\n  '.join(l))
+    print 'static const char *testEncodingsEncoded[%d] = {\n  %s\n};' % (len(l), ',\n  '.join(encoded))
+    print 'static const char *testEncodingsExpected[%d] = {\n  %s\n};' % (len(l), ',\n  '.join(expected))
+else:
+    for i in range(len(expected)):
+        print expected[i]
+        #print '%s: %s' % (expected[i], l[i])
--- a/Resources/Orthanc.doxygen	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Orthanc.doxygen	Thu May 21 16:58:30 2015 +0200
@@ -941,7 +941,7 @@
 # page will contain the date and time when the page was generated. Setting
 # this to NO can help when comparing the output of multiple runs.
 
-HTML_TIMESTAMP         = YES
+HTML_TIMESTAMP         = NO
 
 # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
 # documentation will contain sections that can be hidden and shown after the
--- a/Resources/OrthancClient.doxygen	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/OrthancClient.doxygen	Thu May 21 16:58:30 2015 +0200
@@ -939,7 +939,7 @@
 # page will contain the date and time when the page was generated. Setting
 # this to NO can help when comparing the output of multiple runs.
 
-HTML_TIMESTAMP         = YES
+HTML_TIMESTAMP         = NO
 
 # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
 # documentation will contain sections that can be hidden and shown after the
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/OrthancPlugin.doxygen	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,1794 @@
+# Doxyfile 1.8.1.2
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a hash (#) is considered a comment and will be ignored.
+# The format is:
+#       TAG = value [value, ...]
+# For lists items can also be appended using:
+#       TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (" ").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# http://www.gnu.org/software/libiconv for the list of possible encodings.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or sequence of words) that should
+# identify the project. Note that if you do not use Doxywizard you need
+# to put quotes around the project name if it contains spaces.
+
+PROJECT_NAME           = "Orthanc Plugin SDK"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number.
+# This could be handy for archiving the generated documentation or
+# if some version control system is used.
+
+PROJECT_NUMBER         =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer
+# a quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = "Documentation of the plugin interface of Orthanc"
+
+# With the PROJECT_LOGO tag one can specify an logo or icon that is
+# included in the documentation. The maximum height of the logo should not
+# exceed 55 pixels and the maximum width should not exceed 200 pixels.
+# Doxygen will copy the logo to the output directory.
+
+PROJECT_LOGO           = @CMAKE_SOURCE_DIR@/Resources/OrthancLogoDocumentation.png
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
+# base path where the generated documentation will be put.
+# If a relative path is entered, it will be relative to the location
+# where doxygen was started. If left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = OrthancPluginDocumentation
+
+# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create
+# 4096 sub-directories (in 2 levels) under the output directory of each output
+# format and will distribute the generated files over these directories.
+# Enabling this option can be useful when feeding doxygen a huge amount of
+# source files, where putting all generated files in the same directory would
+# otherwise cause performance problems for the file system.
+
+CREATE_SUBDIRS         = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# The default language is English, other supported languages are:
+# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional,
+# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German,
+# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English
+# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian,
+# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak,
+# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese.
+
+OUTPUT_LANGUAGE        = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will
+# include brief member descriptions after the members that are listed in
+# the file and class documentation (similar to JavaDoc).
+# Set to NO to disable this.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend
+# the brief description of a member or function before the detailed description.
+# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+
+REPEAT_BRIEF           = NO
+
+# This tag implements a quasi-intelligent brief description abbreviator
+# that is used to form the text in various listings. Each string
+# in this list, if found as the leading text of the brief description, will be
+# stripped from the text and the result after processing the whole list, is
+# used as the annotated text. Otherwise, the brief description is used as-is.
+# If left blank, the following values are used ("$name" is automatically
+# replaced with the name of the entity): "The $name class" "The $name widget"
+# "The $name file" "is" "provides" "specifies" "contains"
+# "represents" "a" "an" "the"
+
+ABBREVIATE_BRIEF       =
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# Doxygen will generate a detailed section even if there is only a brief
+# description.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full
+# path before files name in the file list and in the header files. If set
+# to NO the shortest path that makes the file name unique will be used.
+
+FULL_PATH_NAMES        = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag
+# can be used to strip a user-defined part of the path. Stripping is
+# only done if one of the specified strings matches the left-hand part of
+# the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the
+# path to strip.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of
+# the path mentioned in the documentation of a class, which tells
+# the reader which header file to include in order to use a class.
+# If left blank only the name of the header file containing the class
+# definition is used. Otherwise one should specify the include paths that
+# are normally passed to the compiler using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter
+# (but less readable) file names. This can be useful if your file system
+# doesn't support long names like on DOS, Mac, or CD-ROM.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen
+# will interpret the first line (until the first dot) of a JavaDoc-style
+# comment as the brief description. If set to NO, the JavaDoc
+# comments will behave just like regular Qt-style comments
+# (thus requiring an explicit @brief command for a brief description.)
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then Doxygen will
+# interpret the first line (until the first dot) of a Qt-style
+# comment as the brief description. If set to NO, the comments
+# will behave just like regular Qt-style comments (thus requiring
+# an explicit \brief command for a brief description.)
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen
+# treat a multi-line C++ special comment block (i.e. a block of //! or ///
+# comments) as a brief description. This used to be the default behaviour.
+# The new default is to treat a multi-line C++ comment block as a detailed
+# description. Set this tag to YES if you prefer the old behaviour instead.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented
+# member inherits the documentation from any documented member that it
+# re-implements.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce
+# a new page for each member. If set to NO, the documentation of a member will
+# be part of the file/class/namespace that contains it.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab.
+# Doxygen uses this value to replace tabs by spaces in code fragments.
+
+TAB_SIZE               = 8
+
+# This tag can be used to specify a number of aliases that acts
+# as commands in the documentation. An alias has the form "name=value".
+# For example adding "sideeffect=\par Side Effects:\n" will allow you to
+# put the command \sideeffect (or @sideeffect) in the documentation, which
+# will result in a user-defined paragraph with heading "Side Effects:".
+# You can put \n's in the value part of an alias to insert newlines.
+
+ALIASES                =
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name=value". For example adding
+# "class=itcl::class" will allow you to use the command class in the
+# itcl::class meaning.
+
+TCL_SUBST              =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C
+# sources only. Doxygen will then generate output that is more tailored for C.
+# For instance, some of the names that are used will be different. The list
+# of all members will be omitted, etc.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java
+# sources only. Doxygen will then generate output that is more tailored for
+# Java. For instance, namespaces will be presented as packages, qualified
+# scopes will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources only. Doxygen will then generate output that is more tailored for
+# Fortran.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for
+# VHDL.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given extension.
+# Doxygen has a built-in mapping, but you can override or extend it using this
+# tag. The format is ext=language, where ext is a file extension, and language
+# is one of the parsers supported by doxygen: IDL, Java, Javascript, CSharp, C,
+# C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, C++. For instance to make
+# doxygen treat .inc files as Fortran files (default is PHP), and .f files as C
+# (default is Fortran), use: inc=Fortran f=C. Note that for custom extensions
+# you also need to set FILE_PATTERNS otherwise the files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all
+# comments according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you
+# can mix doxygen, HTML, and XML commands with Markdown formatting.
+# Disable only in case of backward compatibilities issues.
+
+MARKDOWN_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should
+# set this tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string); v.s.
+# func(std::string) {}). This also makes the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+
+BUILTIN_STL_SUPPORT    = YES
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only.
+# Doxygen will parse them like normal C++ but will assume all classes use public
+# instead of private inheritance when no explicit protection keyword is present.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate getter
+# and setter methods for a property. Setting this option to YES (the default)
+# will make doxygen replace the get and set methods by a property in the
+# documentation. This will only work if the methods are indeed getting or
+# setting a simple type. If this is not the case, or you want to show the
+# methods anyway, you should set this option to NO.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES, then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# Set the SUBGROUPING tag to YES (the default) to allow class member groups of
+# the same type (for instance a group of public functions) to be put as a
+# subgroup of that type (e.g. under the Public Functions section). Set it to
+# NO to prevent subgrouping. Alternatively, this can be done per class using
+# the \nosubgrouping command.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and
+# unions are shown inside the group in which they are included (e.g. using
+# @ingroup) instead of on a separate page (for HTML and Man pages) or
+# section (for LaTeX and RTF).
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and
+# unions with only public data fields will be shown inline in the documentation
+# of the scope in which they are defined (i.e. file, namespace, or group
+# documentation), provided this scope is documented. If set to NO (the default),
+# structs, classes, and unions are shown on a separate page (for HTML and Man
+# pages) or section (for LaTeX and RTF).
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum
+# is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically
+# be useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to
+# determine which symbols to keep in memory and which to flush to disk.
+# When the cache is full, less often used symbols will be written to disk.
+# For small to medium size projects (<1000 input files) the default value is
+# probably good enough. For larger projects a too small cache size can cause
+# doxygen to be busy swapping symbols to and from disk most of the time
+# causing a significant performance penalty.
+# If the system has enough physical memory increasing the cache will improve the
+# performance by keeping more symbols in memory. Note that the value works on
+# a logarithmic scale so increasing the size by one will roughly double the
+# memory usage. The cache size is given by this formula:
+# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0,
+# corresponding to a cache size of 2^16 = 65536 symbols.
+
+SYMBOL_CACHE_SIZE      = 0
+
+# Similar to the SYMBOL_CACHE_SIZE the size of the symbol lookup cache can be
+# set using LOOKUP_CACHE_SIZE. This cache is used to resolve symbols given
+# their name and scope. Since this can be an expensive process and often the
+# same symbol appear multiple times in the code, doxygen keeps a cache of
+# pre-resolved symbols. If the cache is too small doxygen will become slower.
+# If the cache is too large, memory is wasted. The cache size is given by this
+# formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range is 0..9, the default is 0,
+# corresponding to a cache size of 2^16 = 65536 symbols.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
+# documentation are documented, even if no documentation was available.
+# Private class members and static file members will be hidden unless
+# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL            = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class
+# will be included in the documentation.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal scope will be included in the documentation.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file
+# will be included in the documentation.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs)
+# defined locally in source files will be included in the documentation.
+# If set to NO only classes defined in header files are included.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. When set to YES local
+# methods, which are defined in the implementation section but not in
+# the interface are included in the documentation.
+# If set to NO (the default) only methods in the interface are included.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base
+# name of the file that contains the anonymous namespace. By default
+# anonymous namespaces are hidden.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all
+# undocumented members of documented classes, files or namespaces.
+# If set to NO (the default) these members will be included in the
+# various overviews, but no documentation section is generated.
+# This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy.
+# If set to NO (the default) these classes will be included in the various
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all
+# friend (class|struct|union) declarations.
+# If set to NO (the default) these declarations will be included in the
+# documentation.
+
+HIDE_FRIEND_COMPOUNDS  = YES
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any
+# documentation blocks found inside the body of a function.
+# If set to NO (the default) these blocks will be appended to the
+# function's detailed documentation block.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation
+# that is typed after a \internal command is included. If the tag is set
+# to NO (the default) then the documentation will be excluded.
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate
+# file names in lower-case letters. If set to YES upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+
+CASE_SENSE_NAMES       = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen
+# will show members with their full class and namespace scopes in the
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen
+# will put a list of the files that are included by a file in the documentation
+# of that file.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen
+# will list include files with double quotes in the documentation
+# rather than with sharp brackets.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline]
+# is inserted in the documentation for inline members.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen
+# will sort the (detailed) documentation of file and class members
+# alphabetically by member name. If set to NO the members will appear in
+# declaration order.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the
+# brief documentation of file, namespace and class members alphabetically
+# by member name. If set to NO (the default) the members will appear in
+# declaration order.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen
+# will sort the (brief and detailed) documentation of class members so that
+# constructors and destructors are listed first. If set to NO (the default)
+# the constructors will appear in the respective orders defined by
+# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS.
+# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO
+# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the
+# hierarchy of group names into alphabetical order. If set to NO (the default)
+# the group names will appear in their defined order.
+
+SORT_GROUP_NAMES       = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be
+# sorted by fully-qualified names, including namespaces. If set to
+# NO (the default), the class list will be sorted only by class name,
+# not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the
+# alphabetical list.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to
+# do proper type resolution of all parameters of a function it will reject a
+# match between the prototype and the implementation of a member function even
+# if there is only one candidate or it is obvious which candidate to choose
+# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen
+# will still accept a match between prototype and implementation in such cases.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or
+# disable (NO) the todo list. This list is created by putting \todo
+# commands in the documentation.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or
+# disable (NO) the test list. This list is created by putting \test
+# commands in the documentation.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or
+# disable (NO) the bug list. This list is created by putting \bug
+# commands in the documentation.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or
+# disable (NO) the deprecated list. This list is created by putting
+# \deprecated commands in the documentation.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional
+# documentation sections, marked by \if sectionname ... \endif.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines
+# the initial value of a variable or macro consists of for it to appear in
+# the documentation. If the initializer consists of more lines than specified
+# here it will be hidden. Use a value of 0 to hide initializers completely.
+# The appearance of the initializer of individual variables and macros in the
+# documentation can be controlled using \showinitializer or \hideinitializer
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES  = 0
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated
+# at the bottom of the documentation of classes and structs. If set to YES the
+# list will mention the files that were used to generate the documentation.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page.
+# This will remove the Files entry from the Quick Index and from the
+# Folder Tree View (if specified). The default is YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the
+# Namespaces page.
+# This will remove the Namespaces entry from the Quick Index
+# and from the Folder Tree View (if specified). The default is YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command <command> <input-file>, where <command> is the value of
+# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file
+# provided by doxygen. Whatever the program writes to standard output
+# is used as the file version. See the manual for examples.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option.
+# You can optionally specify a file name after the option, if omitted
+# DoxygenLayout.xml will be used as the name of the layout file.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files
+# containing the references data. This must be a list of .bib files. The
+# .bib extension is automatically appended if omitted. Using this command
+# requires the bibtex tool to be installed. See also
+# http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style
+# of the bibliography can be controlled using LATEX_BIB_STYLE. To use this
+# feature you need bibtex and perl available in the search path.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated
+# by doxygen. Possible values are YES and NO. If left blank NO is used.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated by doxygen. Possible values are YES and NO. If left blank
+# NO is used.
+
+WARNINGS               = YES
+
+# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings
+# for undocumented members. If EXTRACT_ALL is set to YES then this flag will
+# automatically be disabled.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some
+# parameters in a documented function, or documenting parameters that
+# don't exist or using markup commands wrongly.
+
+WARN_IF_DOC_ERROR      = YES
+
+# The WARN_NO_PARAMDOC option can be enabled to get warnings for
+# functions that are documented, but have no documentation for their parameters
+# or return value. If set to NO (the default) doxygen will only warn about
+# wrong or incomplete parameter documentation, but not about the absence of
+# documentation.
+
+WARN_NO_PARAMDOC       = YES
+
+# The WARN_FORMAT tag determines the format of the warning messages that
+# doxygen can produce. The string should contain the $file, $line, and $text
+# tags, which will be replaced by the file and line number from which the
+# warning originated and the warning text. Optionally the format may contain
+# $version, which will be replaced by the version of the file (if it could
+# be obtained via FILE_VERSION_FILTER)
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning
+# and error messages should be written. If left blank the output is written
+# to stderr.
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag can be used to specify the files and/or directories that contain
+# documented source files. You may enter file names like "myfile.cpp" or
+# directories like "/usr/src/myproject". Separate the files or directories
+# with spaces.
+
+INPUT                  = @CMAKE_SOURCE_DIR@/Plugins/Include/OrthancCPlugin.h \
+                         @CMAKE_SOURCE_DIR@/Plugins/Include/OrthancCDatabasePlugin.h \
+                         @CMAKE_SOURCE_DIR@/Plugins/Include/OrthancCppDatabasePlugin.h
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is
+# also the default input encoding. Doxygen uses libiconv (or the iconv built
+# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for
+# the list of possible encodings.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank the following patterns are tested:
+# *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh
+# *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py
+# *.f90 *.f *.for *.vhd *.vhdl
+
+FILE_PATTERNS          = *.h
+
+# The RECURSIVE tag can be used to turn specify whether or not subdirectories
+# should be searched for input files as well. Possible values are YES and NO.
+# If left blank NO is used.
+
+RECURSIVE              = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories. Note that the wildcards are matched
+# against the file with absolute path, so to exclude all test directories
+# for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+
+EXCLUDE_SYMBOLS        = _OrthancPlugin* OrthancPluginGetName
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or
+# directories that contain example code fragments that are included (see
+# the \include command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank all files are included.
+
+EXAMPLE_PATTERNS       =
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude
+# commands irrespective of the value of the RECURSIVE tag.
+# Possible values are YES and NO. If left blank NO is used.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or
+# directories that contain image that are included in the documentation (see
+# the \image command).
+
+IMAGE_PATH             =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command <filter> <input-file>, where <filter>
+# is the value of the INPUT_FILTER tag, and <input-file> is the name of an
+# input file. Doxygen will then use the output that the filter program writes
+# to standard output.
+# If FILTER_PATTERNS is specified, this tag will be
+# ignored.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis.
+# Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match.
+# The filters are a list of the form:
+# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further
+# info on how filters are used. If FILTER_PATTERNS is empty or if
+# non of the patterns match the file name, INPUT_FILTER is applied.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will be used to filter the input files when producing source
+# files to browse (i.e. when SOURCE_BROWSER is set to YES).
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any)
+# and it is also possible to disable source filtering for a specific pattern
+# using *.ext= (so without naming a filter). This option only has effect when
+# FILTER_SOURCE_FILES is enabled.
+
+FILTER_SOURCE_PATTERNS =
+
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will
+# be generated. Documented entities will be cross-referenced with these sources.
+# Note: To get rid of all source code in the generated output, make sure also
+# VERBATIM_HEADERS is set to NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body
+# of functions and classes directly in the documentation.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct
+# doxygen to hide any special comment blocks from generated source code
+# fragments. Normal C, C++ and Fortran comments will always remain visible.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES
+# then for each documented function all documented
+# functions referencing it will be listed.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES
+# then for each documented function all documented entities
+# called/used by that function will be listed.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES (the default)
+# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from
+# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will
+# link to the source code.
+# Otherwise they will link to the documentation.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code
+# will point to the HTML generated by the htags(1) tool instead of doxygen
+# built-in source browser. The htags tool is part of GNU's global source
+# tagging system (see http://www.gnu.org/software/global/global.html). You
+# will need version 4.8.6 or higher.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen
+# will generate a verbatim copy of the header file for each class for
+# which an include is specified. Set to NO to disable this.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index
+# of all compounds will be generated. Enable this if the project
+# contains a lot of classes, structs, unions or interfaces.
+
+ALPHABETICAL_INDEX     = YES
+
+# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then
+# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns
+# in which this list will be split (can be a number in the range [1..20])
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all
+# classes will be put under the same header in the alphabetical index.
+# The IGNORE_PREFIX tag can be used to specify one or more prefixes that
+# should be ignored while generating the index headers.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES (the default) Doxygen will
+# generate HTML output.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `html' will be used as the default path.
+
+HTML_OUTPUT            = doc
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for
+# each generated HTML page (for example: .htm,.php,.asp). If it is left blank
+# doxygen will generate files with .html extension.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a personal HTML header for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard header. Note that when using a custom header you are responsible
+#  for the proper inclusion of any scripts and style sheets that doxygen
+# needs, which is dependent on the configuration options used.
+# It is advised to generate a default header using "doxygen -w html
+# header.html footer.html stylesheet.css YourConfigFile" and then modify
+# that header. Note that the header is subject to change so you typically
+# have to redo this when upgrading to a newer version of doxygen or when
+# changing the value of configuration settings such as GENERATE_TREEVIEW!
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a personal HTML footer for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard footer.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading
+# style sheet that is used by each HTML page. It can be used to
+# fine-tune the look of the HTML output. If the tag is left blank doxygen
+# will generate a default style sheet. Note that doxygen will try to copy
+# the style sheet file to the HTML output directory, so don't put your own
+# style sheet in the HTML output directory as well, or it will be erased!
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath$ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that
+# the files will be copied as-is; there are no commands or markers available.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output.
+# Doxygen will adjust the colors in the style sheet and background images
+# according to this color. Hue is specified as an angle on a colorwheel,
+# see http://en.wikipedia.org/wiki/Hue for more information.
+# For instance the value 0 represents red, 60 is yellow, 120 is green,
+# 180 is cyan, 240 is blue, 300 purple, and 360 is red again.
+# The allowed range is 0 to 359.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of
+# the colors in the HTML output. For a value of 0 the output will use
+# grayscales only. A value of 255 will produce the most vivid colors.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to
+# the luminance component of the colors in the HTML output. Values below
+# 100 gradually make the output lighter, whereas values above 100 make
+# the output darker. The value divided by 100 is the actual gamma applied,
+# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2,
+# and 100 does not change the gamma.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting
+# this to NO can help when comparing the output of multiple runs.
+
+HTML_TIMESTAMP         = NO
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of
+# entries shown in the various tree structured indices initially; the user
+# can expand and collapse entries dynamically later on. Doxygen will expand
+# the tree to such a level that at most the specified number of entries are
+# visible (unless a fully collapsed tree already exceeds this amount).
+# So setting the number of entries 1 will produce a full collapsed tree by
+# default. 0 is a special value representing an infinite number of entries
+# and will result in a full expanded tree by default.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files
+# will be generated that can be used as input for Apple's Xcode 3
+# integrated development environment, introduced with OSX 10.5 (Leopard).
+# To create a documentation set, doxygen will generate a Makefile in the
+# HTML output directory. Running make will produce the docset in that
+# directory and running "make install" will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find
+# it at startup.
+# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+
+GENERATE_DOCSET        = NO
+
+# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the
+# feed. A documentation feed provides an umbrella under which multiple
+# documentation sets from a single provider (such as a company or product suite)
+# can be grouped.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that
+# should uniquely identify the documentation set bundle. This should be a
+# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen
+# will append .docset to the name.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES, additional index files
+# will be generated that can be used as input for tools like the
+# Microsoft HTML help workshop to generate a compiled HTML help file (.chm)
+# of the generated HTML documentation.
+
+GENERATE_HTMLHELP      = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can
+# be used to specify the file name of the resulting .chm file. You
+# can add a path in front of the file if the result should not be
+# written to the html output directory.
+
+CHM_FILE               =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can
+# be used to specify the location (absolute path including file name) of
+# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run
+# the HTML help compiler on the generated index.hhp.
+
+HHC_LOCATION           =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag
+# controls if a separate .chi index file is generated (YES) or that
+# it should be included in the master .chm file (NO).
+
+GENERATE_CHI           = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING
+# is used to encode HtmlHelp index (hhk), content (hhc) and project file
+# content.
+
+CHM_INDEX_ENCODING     =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag
+# controls whether a binary table of contents is generated (YES) or a
+# normal table of contents (NO) in the .chm file.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members
+# to the contents of the HTML help documentation and to the tree view.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated
+# that can be used as input for Qt's qhelpgenerator to generate a
+# Qt Compressed Help (.qch) of the generated HTML documentation.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can
+# be used to specify the file name of the resulting .qch file.
+# The path specified is relative to the HTML output folder.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating
+# Qt Help Project output. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#namespace
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating
+# Qt Help Project output. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#virtual-folders
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to
+# add. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#custom-filters
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see
+# <a href="http://doc.trolltech.com/qthelpproject.html#custom-filters">
+# Qt Help Project / Custom Filters</a>.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's
+# filter section matches.
+# <a href="http://doc.trolltech.com/qthelpproject.html#filter-attributes">
+# Qt Help Project / Filter Attributes</a>.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can
+# be used to specify the location of Qt's qhelpgenerator.
+# If non-empty doxygen will try to run qhelpgenerator on the generated
+# .qhp file.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files
+#  will be generated, which together with the HTML files, form an Eclipse help
+# plugin. To install this plugin and make it available under the help contents
+# menu in Eclipse, the contents of the directory containing the HTML and XML
+# files needs to be copied into the plugins directory of eclipse. The name of
+# the directory within the plugins directory should be the same as
+# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before
+# the help appears.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have
+# this name.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs)
+# at top of each HTML page. The value NO (the default) enables the index and
+# the value YES disables it. Since the tabs have the same information as the
+# navigation tree you can set this option to NO if you already set
+# GENERATE_TREEVIEW to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information.
+# If the tag value is set to YES, a side panel will be generated
+# containing a tree-like index structure (just like the one that
+# is generated for HTML Help). For this to work a browser that supports
+# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser).
+# Windows users are probably better off using the HTML help feature.
+# Since the tree basically has the same information as the tab index you
+# could consider to set DISABLE_INDEX to NO when enabling this option.
+
+GENERATE_TREEVIEW      = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values
+# (range [0,1..20]) that doxygen will group on one line in the generated HTML
+# documentation. Note that a value of 0 will completely suppress the enum
+# values from appearing in the overview section.
+
+ENUM_VALUES_PER_LINE   = 1
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be
+# used to set the initial width (in pixels) of the frame in which the tree
+# is shown.
+
+TREEVIEW_WIDTH         = 250
+
+# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open
+# links to external symbols imported via tag files in a separate window.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# Use this tag to change the font size of Latex formulas included
+# as images in the HTML documentation. The default is 10. Note that
+# when you change the font size after a successful doxygen run you need
+# to manually remove any form_*.png images from the HTML output directory
+# to force them to be regenerated.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are
+# not supported properly for IE 6.0, but are supported on all modern browsers.
+# Note that when changing this option you need to delete any form_*.png files
+# in the HTML output before the changes have effect.
+
+FORMULA_TRANSPARENT    = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax
+# (see http://www.mathjax.org) which uses client side Javascript for the
+# rendering instead of using prerendered bitmaps. Use this if you do not
+# have LaTeX installed or if you want to formulas look prettier in the HTML
+# output. When enabled you may also need to install MathJax separately and
+# configure the path to it using the MATHJAX_RELPATH option.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you need to specify the location relative to the
+# HTML output directory using the MATHJAX_RELPATH option. The destination
+# directory should contain the MathJax.js script. For instance, if the mathjax
+# directory is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to
+# the MathJax Content Delivery Network so you can quickly see the result without
+# installing MathJax.
+# However, it is strongly recommended to install a local
+# copy of MathJax from http://www.mathjax.org before deployment.
+
+MATHJAX_RELPATH        = http://www.mathjax.org/mathjax
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension
+# names that should be enabled during MathJax rendering.
+
+MATHJAX_EXTENSIONS     =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box
+# for the HTML output. The underlying search engine uses javascript
+# and DHTML and should work on any modern browser. Note that when using
+# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets
+# (GENERATE_DOCSET) there is already a search function so this one should
+# typically be disabled. For large projects the javascript based search engine
+# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution.
+
+SEARCHENGINE           = NO
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a PHP enabled web server instead of at the web client
+# using Javascript. Doxygen will generate the search PHP script and index
+# file to put on the web server. The advantage of the server
+# based approach is that it scales better to large projects and allows
+# full text search. The disadvantages are that it is more difficult to setup
+# and does not have live searching capabilities.
+
+SERVER_BASED_SEARCH    = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will
+# generate Latex output.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `latex' will be used as the default path.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked. If left blank `latex' will be used as the default command name.
+# Note that when enabling USE_PDFLATEX this option is only used for
+# generating bitmaps for formulas in the HTML output, but not in the
+# Makefile that is written to the output directory.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to
+# generate index for LaTeX. If left blank `makeindex' will be used as the
+# default command name.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact
+# LaTeX documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used
+# by the printer. Possible values are: a4, letter, legal and
+# executive. If left blank a4wide will be used.
+
+PAPER_TYPE             = a4
+
+# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX
+# packages that should be included in the LaTeX output.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for
+# the generated latex document. The header should contain everything until
+# the first chapter. If it is left blank doxygen will generate a
+# standard header. Notice: only use this tag if you know what you are doing!
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for
+# the generated latex document. The footer should contain everything after
+# the last chapter. If it is left blank doxygen will generate a
+# standard footer. Notice: only use this tag if you know what you are doing!
+
+LATEX_FOOTER           =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated
+# is prepared for conversion to pdf (using ps2pdf). The pdf file will
+# contain links (just like the HTML output) instead of page references
+# This makes the output suitable for online browsing using a pdf viewer.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of
+# plain latex in the generated Makefile. Set this option to YES to get a
+# higher quality PDF documentation.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode.
+# command to the generated LaTeX files. This will instruct LaTeX to keep
+# running if errors occur, instead of asking the user for help.
+# This option is also used when generating formulas in HTML.
+
+LATEX_BATCHMODE        = NO
+
+# If LATEX_HIDE_INDICES is set to YES then doxygen will not
+# include the index chapters (such as File Index, Compound Index, etc.)
+# in the output.
+
+LATEX_HIDE_INDICES     = NO
+
+# If LATEX_SOURCE_CODE is set to YES then doxygen will include
+# source code with syntax highlighting in the LaTeX output.
+# Note that which sources are shown also depends on other settings
+# such as SOURCE_BROWSER.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. The default style is "plain". See
+# http://en.wikipedia.org/wiki/BibTeX for more info.
+
+LATEX_BIB_STYLE        = plain
+
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output
+# The RTF output is optimized for Word 97 and may not look very pretty with
+# other RTF readers or editors.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `rtf' will be used as the default path.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES Doxygen generates more compact
+# RTF documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated
+# will contain hyperlink fields. The RTF file will
+# contain links (just like the HTML output) instead of page references.
+# This makes the output suitable for online browsing using WORD or other
+# programs which support those fields.
+# Note: wordpad (write) and others do not support links.
+
+RTF_HYPERLINKS         = NO
+
+# Load style sheet definitions from file. Syntax is similar to doxygen's
+# config file, i.e. a series of assignments. You only have to provide
+# replacements, missing definitions are set to their default value.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an rtf document.
+# Syntax is similar to doxygen's config file.
+
+RTF_EXTENSIONS_FILE    =
+
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES (the default) Doxygen will
+# generate man pages
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `man' will be used as the default path.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to
+# the generated man pages (default is the subroutine's section .3)
+
+MAN_EXTENSION          = .3
+
+# If the MAN_LINKS tag is set to YES and Doxygen generates man output,
+# then it will generate one additional man file for each entity
+# documented in the real man page(s). These additional files
+# only source the real man page, but without them the man command
+# would be unable to find the correct page. The default is NO.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES Doxygen will
+# generate an XML file that captures the structure of
+# the code including all documentation.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `xml' will be used as the default path.
+
+XML_OUTPUT             = xml
+
+# The XML_SCHEMA tag can be used to specify an XML schema,
+# which can be used by a validating XML parser to check the
+# syntax of the XML files.
+
+XML_SCHEMA             =
+
+# The XML_DTD tag can be used to specify an XML DTD,
+# which can be used by a validating XML parser to check the
+# syntax of the XML files.
+
+XML_DTD                =
+
+# If the XML_PROGRAMLISTING tag is set to YES Doxygen will
+# dump the program listings (including syntax highlighting
+# and cross-referencing information) to the XML output. Note that
+# enabling this will significantly increase the size of the XML output.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will
+# generate an AutoGen Definitions (see autogen.sf.net) file
+# that captures the structure of the code including all
+# documentation. Note that this feature is still experimental
+# and incomplete at the moment.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES Doxygen will
+# generate a Perl module file that captures the structure of
+# the code including all documentation. Note that this
+# feature is still experimental and incomplete at the
+# moment.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES Doxygen will generate
+# the necessary Makefile rules, Perl scripts and LaTeX code to be able
+# to generate PDF and DVI output from the Perl module output.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be
+# nicely formatted so it can be parsed by a human reader.
+# This is useful
+# if you want to understand what is going on.
+# On the other hand, if this
+# tag is set to NO the size of the Perl module output will be much smaller
+# and Perl will parse it just the same.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file
+# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX.
+# This is useful so different doxyrules.make files included by the same
+# Makefile don't overwrite each other's variables.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will
+# evaluate all C-preprocessor directives found in the sources and include
+# files.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro
+# names in the source code. If set to NO (the default) only conditional
+# compilation will be performed. Macro expansion can be done in a controlled
+# way by setting EXPAND_ONLY_PREDEF to YES.
+
+MACRO_EXPANSION        = YES
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES
+# then the macro expansion is limited to the macros specified with the
+# PREDEFINED and EXPAND_AS_DEFINED tags.
+
+EXPAND_ONLY_PREDEF     = YES
+
+# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files
+# pointed to by INCLUDE_PATH will be searched when a #include is found.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by
+# the preprocessor.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will
+# be used.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that
+# are defined before the preprocessor is started (similar to the -D option of
+# gcc). The argument of the tag is a list of macros of the form: name
+# or name=definition (no spaces). If the definition and the = are
+# omitted =1 is assumed. To prevent a macro definition from being
+# undefined via #undef or recursively expanded use the := operator
+# instead of the = operator.
+
+PREDEFINED             = ORTHANC_PLUGIN_INLINE=
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then
+# this tag can be used to specify a list of macro names that should be expanded.
+# The macro definition that is found in the sources will be used.
+# Use the PREDEFINED tag if you want to use a different macro definition that
+# overrules the definition found in the source code.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then
+# doxygen's preprocessor will remove all references to function-like macros
+# that are alone on a line, have an all uppercase name, and do not end with a
+# semicolon, because these will confuse the parser if not removed.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration::additions related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES option can be used to specify one or more tagfiles. For each
+# tag file the location of the external documentation should be added. The
+# format of a tag file without this location is as follows:
+#
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+#
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where "loc1" and "loc2" can be relative or absolute paths
+# or URLs. Note that each tag file must have a unique name (where the name does
+# NOT include the path). If a tag file is not located in the directory in which
+# doxygen is run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create
+# a tag file that is based on the input files it reads.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES all external classes will be listed
+# in the class index. If set to NO only the inherited external classes
+# will be listed.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will
+# be listed.
+
+EXTERNAL_GROUPS        = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of `which perl').
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will
+# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base
+# or super classes. Setting the tag to NO turns the diagrams off. Note that
+# this option also works with HAVE_DOT disabled, but it is recommended to
+# install and use dot, since it yields more powerful graphs.
+
+CLASS_DIAGRAMS         = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see
+# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+MSCGEN_PATH            =
+
+# If set to YES, the inheritance and collaboration graphs will hide
+# inheritance and usage relations if the target is undocumented
+# or is not a class.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz, a graph visualization
+# toolkit from AT&T and Lucent Bell Labs. The other options in this section
+# have no effect if this option is set to NO (the default)
+
+HAVE_DOT               = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is
+# allowed to run in parallel. When set to 0 (the default) doxygen will
+# base this on the number of processors available in the system. You can set it
+# explicitly to a value larger than 0 to get control over the balance
+# between CPU load and processing speed.
+
+DOT_NUM_THREADS        = 0
+
+# By default doxygen will use the Helvetica font for all dot files that
+# doxygen generates. When you want a differently looking font you can specify
+# the font name using DOT_FONTNAME. You need to make sure dot is able to find
+# the font, which can be done by putting it in a standard location or by setting
+# the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the
+# directory containing the font.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs.
+# The default size is 10pt.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the Helvetica font.
+# If you specify a different font using DOT_FONTNAME you can use DOT_FONTPATH to
+# set the path where dot can find it.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect inheritance relations. Setting this tag to YES will force the
+# CLASS_DIAGRAMS tag to NO.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect implementation dependencies (inheritance, containment, and
+# class references variables) of the class with other documented classes.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for groups, showing the direct groups dependencies
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside
+# the class node. If there are many fields or methods and many nodes the
+# graph may become too big to be useful. The UML_LIMIT_NUM_FIELDS
+# threshold limits the number of items for each type to make the size more
+# managable. Set this to 0 for no limit. Note that the threshold may be
+# exceeded by 50% before the limit is enforced.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If set to YES, the inheritance and collaboration graphs will show the
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT
+# tags are set to YES then doxygen will generate a graph for each documented
+# file showing the direct and indirect include dependencies of the file with
+# other documented files.
+
+INCLUDE_GRAPH          = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and
+# HAVE_DOT tags are set to YES then doxygen will generate a graph for each
+# documented header file showing the documented files that directly or
+# indirectly include this file.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH and HAVE_DOT options are set to YES then
+# doxygen will generate a call dependency graph for every global function
+# or class method. Note that enabling this option will significantly increase
+# the time of a run. So in most cases it will be better to enable call graphs
+# for selected functions only using the \callgraph command.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then
+# doxygen will generate a caller dependency graph for every global function
+# or class method. Note that enabling this option will significantly increase
+# the time of a run. So in most cases it will be better to enable caller
+# graphs for selected functions only using the \callergraph command.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen
+# will generate a graphical hierarchy of all classes instead of a textual one.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH and HAVE_DOT tags are set to YES
+# then doxygen will show the dependencies a directory has on other directories
+# in a graphical way. The dependency relations are determined by the #include
+# relations between the files in the directories.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. Possible values are svg, png, jpg, or gif.
+# If left blank png will be used. If you choose svg you need to set
+# HTML_FILE_EXTENSION to xhtml in order to make the SVG files
+# visible in IE 9+ (other browsers do not have this requirement).
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+# Note that this requires a modern browser other than Internet Explorer.
+# Tested and working are Firefox, Chrome, Safari, and Opera. For IE 9+ you
+# need to set HTML_FILE_EXTENSION to xhtml in order to make the SVG files
+# visible. Older versions of IE do not have SVG support.
+
+INTERACTIVE_SVG        = NO
+
+# The tag DOT_PATH can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the
+# \dotfile command).
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the
+# \mscfile command).
+
+MSCFILE_DIRS           =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of
+# nodes that will be shown in the graph. If the number of nodes in a graph
+# becomes larger than this value, doxygen will truncate the graph, which is
+# visualized by representing a node as a red box. Note that doxygen if the
+# number of direct children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note
+# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the
+# graphs generated by dot. A depth value of 3 means that only nodes reachable
+# from the root by following a path via at most 3 edges will be shown. Nodes
+# that lay further from the root node will be omitted. Note that setting this
+# option to 1 or 2 may greatly reduce the computation time needed for large
+# code bases. Also note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not
+# seem to support this out of the box. Warning: Depending on the platform used,
+# enabling this option may lead to badly anti-aliased labels on the edges of
+# a graph (i.e. they become hard to read).
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10)
+# support this, this feature is disabled by default.
+
+DOT_MULTI_TARGETS      = YES
+
+# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will
+# generate a legend page explaining the meaning of the various boxes and
+# arrows in the dot generated graphs.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will
+# remove the intermediate dot files that are used to generate
+# the various graphs.
+
+DOT_CLEANUP            = YES
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/boost-1.55.0-clang-atomic.patch	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,87 @@
+--- boost/atomic/detail/cas128strong.hpp
++++ boost/atomic/detail/cas128strong.hpp
+@@ -196,15 +196,17 @@ class base_atomic<T, void, 16, Sign>
+ 
+ public:
+     BOOST_DEFAULTED_FUNCTION(base_atomic(void), {})
+-    explicit base_atomic(value_type const& v) BOOST_NOEXCEPT : v_(0)
++    explicit base_atomic(value_type const& v) BOOST_NOEXCEPT
+     {
++        memset(&v_, 0, sizeof(v_));
+         memcpy(&v_, &v, sizeof(value_type));
+     }
+ 
+     void
+     store(value_type const& value, memory_order order = memory_order_seq_cst) volatile BOOST_NOEXCEPT
+     {
+-        storage_type value_s = 0;
++        storage_type value_s;
++        memset(&value_s, 0, sizeof(value_s));
+         memcpy(&value_s, &value, sizeof(value_type));
+         platform_fence_before_store(order);
+         platform_store128(value_s, &v_);
+@@ -247,7 +249,9 @@ class base_atomic<T, void, 16, Sign>
+         memory_order success_order,
+         memory_order failure_order) volatile BOOST_NOEXCEPT
+     {
+-        storage_type expected_s = 0, desired_s = 0;
++        storage_type expected_s, desired_s;
++        memset(&expected_s, 0, sizeof(expected_s));
++        memset(&desired_s, 0, sizeof(desired_s));
+         memcpy(&expected_s, &expected, sizeof(value_type));
+         memcpy(&desired_s, &desired, sizeof(value_type));
+ 
+--- boost/atomic/detail/gcc-atomic.hpp
++++ boost/atomic/detail/gcc-atomic.hpp
+@@ -958,14 +958,16 @@ class base_atomic<T, void, 16, Sign>
+ 
+ public:
+     BOOST_DEFAULTED_FUNCTION(base_atomic(void), {})
+-    explicit base_atomic(value_type const& v) BOOST_NOEXCEPT : v_(0)
++    explicit base_atomic(value_type const& v) BOOST_NOEXCEPT
+     {
++        memset(&v_, 0, sizeof(v_));
+         memcpy(&v_, &v, sizeof(value_type));
+     }
+ 
+     void store(value_type const& v, memory_order order = memory_order_seq_cst) volatile BOOST_NOEXCEPT
+     {
+-        storage_type tmp = 0;
++        storage_type tmp;
++        memset(&tmp, 0, sizeof(tmp));
+         memcpy(&tmp, &v, sizeof(value_type));
+         __atomic_store_n(&v_, tmp, atomics::detail::convert_memory_order_to_gcc(order));
+     }
+@@ -980,7 +982,8 @@ class base_atomic<T, void, 16, Sign>
+ 
+     value_type exchange(value_type const& v, memory_order order = memory_order_seq_cst) volatile BOOST_NOEXCEPT
+     {
+-        storage_type tmp = 0;
++        storage_type tmp;
++        memset(&tmp, 0, sizeof(tmp));
+         memcpy(&tmp, &v, sizeof(value_type));
+         tmp = __atomic_exchange_n(&v_, tmp, atomics::detail::convert_memory_order_to_gcc(order));
+         value_type res;
+@@ -994,7 +997,9 @@ class base_atomic<T, void, 16, Sign>
+         memory_order success_order,
+         memory_order failure_order) volatile BOOST_NOEXCEPT
+     {
+-        storage_type expected_s = 0, desired_s = 0;
++        storage_type expected_s, desired_s;
++        memset(&expected_s, 0, sizeof(expected_s));
++        memset(&desired_s, 0, sizeof(desired_s));
+         memcpy(&expected_s, &expected, sizeof(value_type));
+         memcpy(&desired_s, &desired, sizeof(value_type));
+         const bool success = __atomic_compare_exchange_n(&v_, &expected_s, desired_s, false,
+@@ -1010,7 +1015,9 @@ class base_atomic<T, void, 16, Sign>
+         memory_order success_order,
+         memory_order failure_order) volatile BOOST_NOEXCEPT
+     {
+-        storage_type expected_s = 0, desired_s = 0;
++        storage_type expected_s, desired_s;
++        memset(&expected_s, 0, sizeof(expected_s));
++        memset(&desired_s, 0, sizeof(desired_s));
+         memcpy(&expected_s, &expected, sizeof(value_type));
+         memcpy(&desired_s, &desired, sizeof(value_type));
+         const bool success = __atomic_compare_exchange_n(&v_, &expected_s, desired_s, true,
+-- 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/dcmtk-linux-speed.patch	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,24 @@
+diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dul.cc dcmtk-3.6.0/dcmnet/libsrc/dul.cc
+--- dcmtk-3.6.0.orig/dcmnet/libsrc/dul.cc	2010-12-01 09:26:36.000000000 +0100
++++ dcmtk-3.6.0/dcmnet/libsrc/dul.cc	2015-05-15 17:03:50.762451757 +0200
+@@ -1840,7 +1840,7 @@
+     }
+ #endif
+ #endif
+-    setTCPBufferLength(sock);
++    //setTCPBufferLength(sock);
+ 
+ #ifndef DONT_DISABLE_NAGLE_ALGORITHM
+     /*
+diff -urEb dcmtk-3.6.0.orig/dcmnet/libsrc/dulfsm.cc dcmtk-3.6.0/dcmnet/libsrc/dulfsm.cc
+--- dcmtk-3.6.0.orig/dcmnet/libsrc/dulfsm.cc	2010-12-01 09:26:36.000000000 +0100
++++ dcmtk-3.6.0/dcmnet/libsrc/dulfsm.cc	2015-05-15 17:03:55.570451952 +0200
+@@ -2417,7 +2417,7 @@
+           return makeDcmnetCondition(DULC_TCPINITERROR, OF_error, msg.c_str());
+         }
+ #endif
+-        setTCPBufferLength(s);
++        //setTCPBufferLength(s);
+ 
+ #ifndef DONT_DISABLE_NAGLE_ALGORITHM
+         /*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/glog-visual-studio-port.h	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,178 @@
+/* Copyright (c) 2008, Google Inc.
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * ---
+ * Author: Craig Silverstein
+ * Copied from google-perftools and modified by Shinichiro Hamaji
+ *
+ * These are some portability typedefs and defines to make it a bit
+ * easier to compile this code under VC++.
+ *
+ * Several of these are taken from glib:
+ *    http://developer.gnome.org/doc/API/glib/glib-windows-compatability-functions.html
+ */
+
+#ifndef CTEMPLATE_WINDOWS_PORT_H_
+#define CTEMPLATE_WINDOWS_PORT_H_
+
+#include "config.h"
+
+#ifdef _WIN32
+
+#define WIN32_LEAN_AND_MEAN  /* We always want minimal includes */
+#include <windows.h>
+#include <winsock.h>         /* for gethostname */
+#include <io.h>              /* because we so often use open/close/etc */
+#include <direct.h>          /* for _getcwd() */
+#include <process.h>         /* for _getpid() */
+#include <stdio.h>           /* read in vsnprintf decl. before redifining it */
+#include <stdarg.h>          /* template_dictionary.cc uses va_copy */
+#include <string.h>          /* for _strnicmp(), strerror_s() */
+#include <time.h>            /* for localtime_s() */
+/* Note: the C++ #includes are all together at the bottom.  This file is
+ * used by both C and C++ code, so we put all the C++ together.
+ */
+
+// Fix by Sebastien Jodogne for Visual Studio 2013
+// https://code.google.com/p/google-glog/issues/detail?id=212
+#if defined(_MSC_VER) && (_MSC_VER >= 1800)
+#include <algorithm>
+#endif
+
+/* 4244: otherwise we get problems when substracting two size_t's to an int
+ * 4251: it's complaining about a private struct I've chosen not to dllexport
+ * 4355: we use this in a constructor, but we do it safely
+ * 4715: for some reason VC++ stopped realizing you can't return after abort()
+ * 4800: we know we're casting ints/char*'s to bools, and we're ok with that
+ * 4996: Yes, we're ok using "unsafe" functions like fopen() and strerror()
+ */
+#pragma warning(disable:4244 4251 4355 4715 4800 4996)
+
+/* file I/O */
+#define PATH_MAX 1024
+#define access  _access
+#define getcwd  _getcwd
+#define open    _open
+#define read    _read
+#define write   _write
+#define lseek   _lseek
+#define close   _close
+#define popen   _popen
+#define pclose  _pclose
+#define R_OK    04           /* read-only (for access()) */
+#define S_ISDIR(m)  (((m) & _S_IFMT) == _S_IFDIR)
+#ifndef __MINGW32__
+enum { STDIN_FILENO = 0, STDOUT_FILENO = 1, STDERR_FILENO = 2 };
+#endif
+#define S_IRUSR S_IREAD
+#define S_IWUSR S_IWRITE
+
+/* Not quite as lightweight as a hard-link, but more than good enough for us. */
+#define link(oldpath, newpath)  CopyFileA(oldpath, newpath, false)
+
+#define strcasecmp   _stricmp
+#define strncasecmp  _strnicmp
+
+/* In windows-land, hash<> is called hash_compare<> (from xhash.h) */
+#if _MSC_VER < 1700
+#define hash  hash_compare
+#endif
+
+/* Sleep is in ms, on windows */
+#define sleep(secs)  Sleep((secs) * 1000)
+
+/* We can't just use _vsnprintf and _snprintf as drop-in-replacements,
+ * because they don't always NUL-terminate. :-(  We also can't use the
+ * name vsnprintf, since windows defines that (but not snprintf (!)).
+ */
+extern int snprintf(char *str, size_t size,
+                                       const char *format, ...);
+extern int safe_vsnprintf(char *str, size_t size,
+                          const char *format, va_list ap);
+#define vsnprintf(str, size, format, ap)  safe_vsnprintf(str, size, format, ap)
+#define va_copy(dst, src)  (dst) = (src)
+
+/* Windows doesn't support specifying the number of buckets as a
+ * hash_map constructor arg, so we leave this blank.
+ */
+#define CTEMPLATE_SMALL_HASHTABLE
+
+#define DEFAULT_TEMPLATE_ROOTDIR  ".."
+
+// ----------------------------------- SYSTEM/PROCESS
+typedef int pid_t;
+#define getpid  _getpid
+
+// ----------------------------------- THREADS
+typedef DWORD pthread_t;
+typedef DWORD pthread_key_t;
+typedef LONG pthread_once_t;
+enum { PTHREAD_ONCE_INIT = 0 };   // important that this be 0! for SpinLock
+#define pthread_self  GetCurrentThreadId
+#define pthread_equal(pthread_t_1, pthread_t_2)  ((pthread_t_1)==(pthread_t_2))
+
+#if defined(__MINGW32__)
+inline int localtime_s(tm * _tm, const time_t * time)
+{
+  tm * posix_local_time_struct = localtime(time);
+  if (posix_local_time_struct == NULL) 
+  {
+    return 1;
+  }
+
+  *_tm = *posix_local_time_struct;
+
+  return 0;
+}
+
+inline char* strerror_s(char* buf, size_t buflen, int errnum) 
+{
+  const char* str = strerror(errnum);
+  return strncpy(buf, str, buflen - 1);
+}
+#endif
+
+inline struct tm* localtime_r(const time_t* timep, struct tm* result) {
+  localtime_s(result, timep);
+  return result;
+}
+
+inline char* strerror_r(int errnum, char* buf, size_t buflen) {
+  strerror_s(buf, buflen, errnum);
+  return buf;
+}
+
+#ifndef __cplusplus
+/* I don't see how to get inlining for C code in MSVC.  Ah well. */
+#define inline
+#endif
+
+#endif  /* _WIN32 */
+
+#endif  /* CTEMPLATE_WINDOWS_PORT_H_ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/mongoose-3.1-patch.diff	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,54 @@
+--- /home/jodogne/Subversion/Orthanc/ThirdPartyDownloads/mongoose/mongoose.c	2012-03-11 23:41:35.000000000 +0100
++++ mongoose.c	2013-03-07 10:07:00.566266153 +0100
+@@ -92,8 +92,9 @@
+ #define strtoll(x, y, z) strtol(x, y, z)
+ #else
+ #define __func__  __FUNCTION__
+-#define strtoull(x, y, z) _strtoui64(x, y, z)
+-#define strtoll(x, y, z) _strtoi64(x, y, z)
++#include <stdlib.h>
++//#define strtoull(x, y, z) _strtoui64(x, y, z)
++//#define strtoll(x, y, z) _strtoi64(x, y, z)
+ #endif // _MSC_VER
+ 
+ #define ERRNO   GetLastError()
+@@ -253,6 +254,14 @@
+ #define MSG_NOSIGNAL 0
+ #endif
+ 
++#if __gnu_hurd__ == 1
++/**
++ * There is no limit on the length on a path under GNU Hurd, so we set
++ * it to an arbitrary constant.
++ **/
++#define PATH_MAX 4096
++#endif
++
+ typedef void * (*mg_thread_func_t)(void *);
+ 
+ static const char *http_500_error = "Internal Server Error";
+@@ -3844,10 +3853,8 @@
+ }
+ 
+ static void discard_current_request_from_buffer(struct mg_connection *conn) {
+-  char *buffered;
+   int buffered_len, body_len;
+ 
+-  buffered = conn->buf + conn->request_len;
+   buffered_len = conn->data_len - conn->request_len;
+   assert(buffered_len >= 0);
+ 
+@@ -4148,7 +4155,13 @@
+ 
+   // Wait until mg_fini() stops
+   while (ctx->stop_flag != 2) {
++#if defined(__linux)
++    usleep(100000);
++#elif defined(_WIN32)
++    Sleep(100);
++#else
+     (void) sleep(0);
++#endif
+   }
+   free_context(ctx);
+ 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Patches/mongoose-3.8-patch.diff	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,29 @@
+--- mongoose.c.orig	2014-09-01 11:25:18.223466994 +0200
++++ mongoose.c	2014-09-01 11:30:21.807479338 +0200
+@@ -50,6 +50,14 @@
+ #define PATH_MAX FILENAME_MAX
+ #endif // __SYMBIAN32__
+ 
++#if __gnu_hurd__ == 1
++/**
++ * There is no limit on the length on a path under GNU Hurd, so we set
++ * it to an arbitrary constant.
++ **/
++#define PATH_MAX 4096
++#endif
++
+ #ifndef _WIN32_WCE // Some ANSI #includes are not available on Windows CE
+ #include <sys/types.h>
+ #include <sys/stat.h>
+@@ -108,8 +116,9 @@
+ #define strtoll(x, y, z) _atoi64(x)
+ #else
+ #define __func__  __FUNCTION__
+-#define strtoull(x, y, z) _strtoui64(x, y, z)
+-#define strtoll(x, y, z) _strtoi64(x, y, z)
++#include <stdlib.h>
++//#define strtoull(x, y, z) _strtoui64(x, y, z)
++//#define strtoll(x, y, z) _strtoi64(x, y, z)
+ #endif // _MSC_VER
+ 
+ #define ERRNO   GetLastError()
--- a/Resources/Patches/mongoose-patch.diff	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
---- /home/jodogne/Subversion/Orthanc/ThirdPartyDownloads/mongoose/mongoose.c	2012-03-11 23:41:35.000000000 +0100
-+++ mongoose.c	2013-03-07 10:07:00.566266153 +0100
-@@ -92,8 +92,9 @@
- #define strtoll(x, y, z) strtol(x, y, z)
- #else
- #define __func__  __FUNCTION__
--#define strtoull(x, y, z) _strtoui64(x, y, z)
--#define strtoll(x, y, z) _strtoi64(x, y, z)
-+#include <stdlib.h>
-+//#define strtoull(x, y, z) _strtoui64(x, y, z)
-+//#define strtoll(x, y, z) _strtoi64(x, y, z)
- #endif // _MSC_VER
- 
- #define ERRNO   GetLastError()
-@@ -253,6 +254,14 @@
- #define MSG_NOSIGNAL 0
- #endif
- 
-+#if __gnu_hurd__ == 1
-+/**
-+ * There is no limit on the length on a path under GNU Hurd, so we set
-+ * it to an arbitrary constant.
-+ **/
-+#define PATH_MAX 4096
-+#endif
-+
- typedef void * (*mg_thread_func_t)(void *);
- 
- static const char *http_500_error = "Internal Server Error";
-@@ -3844,10 +3853,8 @@
- }
- 
- static void discard_current_request_from_buffer(struct mg_connection *conn) {
--  char *buffered;
-   int buffered_len, body_len;
- 
--  buffered = conn->buf + conn->request_len;
-   buffered_len = conn->data_len - conn->request_len;
-   assert(buffered_len >= 0);
- 
-@@ -4148,7 +4155,13 @@
- 
-   // Wait until mg_fini() stops
-   while (ctx->stop_flag != 2) {
-+#if defined(__linux)
-+    usleep(100000);
-+#elif defined(_WIN32)
-+    Sleep(100);
-+#else
-     (void) sleep(0);
-+#endif
-   }
-   free_context(ctx);
- 
--- a/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Thu May 21 16:58:30 2015 +0200
@@ -1,8 +1,8 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
-# Belgium
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Lua/Autorouting.lua	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,3 @@
+function OnStoredInstance(instanceId, tags, metadata)
+   Delete(SendToModality(instanceId, 'sample'))
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Lua/AutoroutingConditional.lua	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,19 @@
+function OnStoredInstance(instanceId, tags, metadata, remoteAet, calledAet)
+   -- The "remoteAet" and "calledAet" arguments are only available
+   -- since Orthanc 0.8.6
+   if remoteAet ~=nil and calledAet ~= nil then
+      print ("Source AET: " .. remoteAet .. " => Called AET: " .. calledAet)
+   end
+
+   -- Extract the value of the "PatientName" DICOM tag
+   local patientName = string.lower(tags['PatientName'])
+
+   if string.find(patientName, 'david') ~= nil then
+      -- Only send patients whose name contains "David"
+      Delete(SendToModality(instanceId, 'sample'))
+
+   else
+      -- Delete the patients that are not called "David"
+      Delete(instanceId)
+   end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Lua/AutoroutingModification.lua	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,21 @@
+function OnStoredInstance(instanceId, tags, metadata)
+   -- Ignore the instances that result from a modification to avoid
+   -- infinite loops
+   if (metadata['ModifiedFrom'] == nil and
+       metadata['AnonymizedFrom'] == nil) then
+
+      -- The tags to be replaced
+      local replace = {}
+      replace['StationName'] = 'My Medical Device'
+      replace['0031-1020'] = 'Some private tag'
+
+      -- The tags to be removed
+      local remove = { 'MilitaryRank' }
+
+      -- Modify the instance, send it, then delete the modified instance
+      Delete(SendToModality(ModifyInstance(instanceId, replace, remove, true), 'sample'))
+
+      -- Delete the original instance
+      Delete(instanceId)
+   end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Lua/CallDcm2Xml.lua	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,8 @@
+function OnStoredInstance(instanceId, tags, metadata)
+   -- Assume Latin1 encoding in dcm2xml
+   local args = {}
+   table.insert(args, '+Ca')
+   table.insert(args, 'latin-1')
+
+   Delete(CallSystem(instanceId, 'dcm2xml', args))
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Lua/CallWebService.js	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,80 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ **/
+
+
+/**
+ * This file is a simple echo Web service implemented using
+ * "node.js". Whenever it receives a POST HTTP query, it echoes its
+ * body both to stdout and to the client. Credentials are checked.
+ **/
+
+
+// Parameters of the ECHO server 
+var port = 8000;
+var username = 'alice';
+var password = 'alicePassword';
+
+
+var http = require('http');
+var authorization = 'Basic ' + new Buffer(username + ':' + password).toString('base64')
+
+var server = http.createServer(function(req, response) {
+  // Check the credentials
+  if (req.headers.authorization != authorization)
+  {
+    console.log('Bad credentials, access not allowed');
+    response.writeHead(401);
+    response.end();
+    return;
+  }
+
+  switch (req.method)
+  {
+  case 'POST':
+    {
+      var body = '';
+
+      req.on('data', function (data) {
+        response.write(data);
+        body += data;
+      });
+
+      req.on('end', function () {
+        console.log('Message received: ' + body);
+        response.end();
+      });
+
+      break;
+    }
+
+  default:
+    console.log('Method ' + req.method + ' is not supported by this ECHO Web service');
+    response.writeHead(405, {'Allow': 'POST'});
+    response.end();
+  }
+});
+
+server.listen(port);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Lua/CallWebService.lua	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,24 @@
+-- This sample shows how to call a remote Web service whenever an
+-- instance is received by Orthanc. For this sample to work, you have
+-- to start the "CallWebService.js" script next to this file using
+-- NodeJs.
+
+-- 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'))) ()
+
+SetHttpCredentials('alice', 'alicePassword')
+
+function OnStoredInstance(instanceId, tags, metadata)
+   -- Build the POST body
+   local info = {}
+   info['InstanceID'] = instanceId
+   info['PatientName'] = tags['PatientName']
+   info['PatientID'] = tags['PatientID']
+
+   -- Send the POST request
+   local answer = HttpPost('http://localhost:8000/', JSON:encode(info))
+
+   -- The answer equals "ERROR" in case of an error
+   print('Web service called, answer received: ' .. answer)
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Lua/TransferSyntaxDisable.lua	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,29 @@
+function IsDeflatedTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpegTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpeg2000TransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpegLosslessTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpipTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsMpeg2TransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsRleTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+print('All special transfer syntaxes are now disallowed')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Lua/TransferSyntaxEnable.lua	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,29 @@
+function IsDeflatedTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpegTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpeg2000TransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpegLosslessTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpipTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsMpeg2TransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsRleTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+print('All special transfer syntaxes are now accepted')
--- a/Resources/Samples/OrthancClient/Basic/main.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/OrthancClient/Basic/main.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * Permission is hereby granted, free of charge, to any person
  * obtaining a copy of this software and associated documentation
@@ -54,6 +54,15 @@
           OrthancClient::Series series(study.GetSeries(k));
           std::cout << "    Series: " << series.GetId() << std::endl;
 
+          if (series.Is3DImage())
+          {
+            std::cout << "    This is a 3D image whose voxel size is " 
+                      << series.GetVoxelSizeX() << " x " 
+                      << series.GetVoxelSizeY() << " x " 
+                      << series.GetVoxelSizeZ() << ", and slice thickness is " 
+                      << series.GetSliceThickness() << std::endl;
+          }
+
           for (unsigned int l = 0; l < series.GetInstanceCount(); l++)
           {
             std::cout << "      Instance: " << series.GetInstance(l).GetId() << std::endl;
--- a/Resources/Samples/OrthancClient/Vtk/main.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/OrthancClient/Vtk/main.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * Permission is hereby granted, free of charge, to any person
  * obtaining a copy of this software and associated documentation
--- a/Resources/Samples/Python/AnonymizeAllPatients.py	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/Python/AnonymizeAllPatients.py	Thu May 21 16:58:30 2015 +0200
@@ -2,8 +2,8 @@
 # -*- coding: utf-8 -*-
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
-# Belgium
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Python/ArchiveStudiesInTimeRange.py	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+
+import os
+import os.path
+import sys
+import RestToolbox
+
+def PrintHelp():
+    print('Download ZIP archives for all the studies generated '
+          'during a given time range (according to the StudyDate tag)\n')
+    print('Usage: %s <URL> <StartDate> <EndDate> <TargetFolder>\n' % sys.argv[0])
+    print('Example: %s http://localhost:8042/ 20150101 20151231 /tmp/\n' % sys.argv[0])
+    exit(-1)
+
+def CheckIsDate(date):
+    if len(date) != 8 or not date.isdigit():
+        print '"%s" is not a valid date!\n' % date
+        exit(-1)
+
+
+if len(sys.argv) != 5:
+    PrintHelp()
+
+URL = sys.argv[1]
+START = sys.argv[2]
+END = sys.argv[3]
+TARGET = sys.argv[4]
+
+CheckIsDate(START)
+CheckIsDate(END)
+
+# Loop over the studies
+for studyId in RestToolbox.DoGet('%s/studies' % URL):
+    # Retrieve the DICOM tags of the current study
+    study = RestToolbox.DoGet('%s/studies/%s' % (URL, studyId))['MainDicomTags']
+
+    # Retrieve the DICOM tags of the parent patient of this study
+    patient = RestToolbox.DoGet('%s/studies/%s/patient' % (URL, studyId))['MainDicomTags']
+
+    # Check that the StudyDate tag lies within the given range
+    studyDate = study['StudyDate'][:8]
+    if studyDate >= START and studyDate <= END:
+        # Create a filename
+        filename = '%s - %s %s - %s.zip' % (study['StudyDate'],
+                                            patient['PatientID'],
+                                            patient['PatientName'],
+                                            study['StudyDescription'])
+
+        # Remove any non-ASCII character in the filename
+        filename = filename.encode('ascii', errors = 'replace')
+
+        # Download the ZIP archive of the study
+        print('Downloading %s' % filename)
+        zipContent = RestToolbox.DoGet('%s/studies/%s/archive' % (URL, studyId))
+
+        # Write the ZIP archive at the proper location
+        with open(os.path.join(TARGET, filename), 'wb') as f:
+            f.write(zipContent)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Python/AutoClassify.py	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,124 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import argparse
+import time
+import os
+import os.path
+import sys
+import RestToolbox
+
+parser = argparse.ArgumentParser(
+    description = 'Automated classification of DICOM files from Orthanc.',
+    formatter_class = argparse.ArgumentDefaultsHelpFormatter)
+
+parser.add_argument('--host', default = 'localhost',
+                    help = 'The host address that runs Orthanc')
+parser.add_argument('--port', type = int, default = '8042',
+                    help = 'The port number to which Orthanc is listening for the REST API')
+parser.add_argument('--target', default = 'OrthancFiles',
+                    help = 'The target directory where to store the DICOM files')
+parser.add_argument('--all', action = 'store_true',
+                    help = 'Replay the entire history on startup (disabled by default)')
+parser.set_defaults(all = False)
+parser.add_argument('--remove', action = 'store_true',
+                    help = 'Remove DICOM files from Orthanc once classified (disabled by default)')
+parser.set_defaults(remove = False)
+
+
+def FixPath(p):
+    return p.encode('ascii', 'ignore').strip().decode()
+
+def GetTag(resource, tag):
+    if ('MainDicomTags' in resource and
+        tag in resource['MainDicomTags']):
+        return resource['MainDicomTags'][tag]
+    else:
+        return 'No' + tag
+
+def ClassifyInstance(instanceId):
+    # Extract the patient, study, series and instance information
+    instance = RestToolbox.DoGet('%s/instances/%s' % (URL, instanceId))
+    series = RestToolbox.DoGet('%s/series/%s' % (URL, instance['ParentSeries']))
+    study = RestToolbox.DoGet('%s/studies/%s' % (URL, series['ParentStudy']))
+    patient = RestToolbox.DoGet('%s/patients/%s' % (URL, study['ParentPatient']))
+
+    # Construct a target path
+    a = '%s - %s' % (GetTag(patient, 'PatientID'),
+                     GetTag(patient, 'PatientName'))
+    b = GetTag(study, 'StudyDescription')
+    c = '%s - %s' % (GetTag(series, 'Modality'),
+                     GetTag(series, 'SeriesDescription'))
+    d = '%s.dcm' % GetTag(instance, 'SOPInstanceUID')
+    
+    p = os.path.join(args.target, FixPath(a), FixPath(b), FixPath(c))
+    f = os.path.join(p, FixPath(d))
+
+    # Copy the DICOM file to the target path
+    print('Writing new DICOM file: %s' % f)
+    
+    try:
+        os.makedirs(p)
+    except:
+        # Already existing directory, ignore the error
+        pass
+    
+    dcm = RestToolbox.DoGet('%s/instances/%s/file' % (URL, instanceId))
+    with open(f, 'wb') as g:
+        g.write(dcm)
+
+
+# Parse the arguments
+args = parser.parse_args()
+URL = 'http://%s:%d' % (args.host, args.port)
+print('Connecting to Orthanc on address: %s' % URL)
+
+# Compute the starting point for the changes loop
+if args.all:
+    current = 0
+else:
+    current = RestToolbox.DoGet(URL + '/changes?last')['Last']
+
+# Polling loop using the 'changes' API of Orthanc, waiting for the
+# incoming of new DICOM files
+while True:
+    r = RestToolbox.DoGet(URL + '/changes', {
+            'since' : current,
+            'limit' : 4   # Retrieve at most 4 changes at once
+            })
+
+    for change in r['Changes']:
+        # We are only interested interested in the arrival of new instances
+        if change['ChangeType'] == 'NewInstance':
+            try:
+                ClassifyInstance(change['ID'])
+            except:
+                print('Unable to write instance %s to the disk' % change['ID'])
+
+            # If requested, remove the instance once it has been copied
+            if args.remove:
+                RestToolbox.DoDelete('%s/instances/%s' % (URL, change['ID']))
+
+    current = r['Last']
+
+    if r['Done']:
+        print('Everything has been processed: Waiting...')
+        time.sleep(1)
--- a/Resources/Samples/Python/ChangesLoop.py	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/Python/ChangesLoop.py	Thu May 21 16:58:30 2015 +0200
@@ -1,8 +1,8 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
-# Belgium
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -54,7 +54,7 @@
     # Remove the possible trailing characters due to DICOM padding
     patientName = patientName.strip()
 
-    print 'New instance received for patient "%s": "%s"' % (patientName, path)
+    print('New instance received for patient "%s": "%s"' % (patientName, path))
 
 
 
@@ -82,5 +82,5 @@
     current = r['Last']
 
     if r['Done']:
-        print "Everything has been processed: Waiting..."
+        print('Everything has been processed: Waiting...')
         time.sleep(1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Python/ContinuousPatientAnonymization.py	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,99 @@
+#!/usr/bin/python
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+
+import time
+import sys
+import RestToolbox
+import md5
+
+
+##
+## Print help message
+##
+
+if len(sys.argv) != 3:
+    print("""
+Sample script that anonymizes patients in real-time. A patient gets
+anonymized as soon as she gets stable (i.e. when no DICOM instance has
+been received for this patient for a sufficient amount of time - cf.
+the configuration option "StableAge").
+
+Usage: %s [hostname] [HTTP port]
+For instance: %s localhost 8042
+""" % (sys.argv[0], sys.argv[0]))
+    exit(-1)
+
+URL = 'http://%s:%d' % (sys.argv[1], int(sys.argv[2]))
+
+
+
+##
+## The following function is called whenever a patient gets stable
+##
+
+COUNT = 1
+
+def AnonymizePatient(path):
+    global URL
+    global COUNT
+
+    patient = RestToolbox.DoGet(URL + path)
+    patientID = patient['MainDicomTags']['PatientID']
+
+    # Ignore anonymized patients
+    if not 'AnonymizedFrom' in patient:
+        print('Patient with ID "%s" is stabilized: anonymizing it...' % (patientID))
+        
+        # The PatientID after anonymization is taken as the 8 first
+        # characters from the MD5 hash of the original PatientID
+        anonymizedID = md5.new(patientID).hexdigest()[:8]
+        anonymizedName = 'Anonymized patient %d' % COUNT
+        COUNT += 1
+
+        RestToolbox.DoPost(URL + path + '/anonymize',
+                           { 'Replace' : { 'PatientID' : anonymizedID,
+                                           'PatientName' : anonymizedName } })
+
+        # Delete the source patient after the anonymization
+        RestToolbox.DoDelete(URL + change['Path'])
+
+
+
+##
+## Main loop that listens to the changes API.
+## 
+
+current = 0
+while True:
+    r = RestToolbox.DoGet(URL + '/changes', {
+            'since' : current,
+            'limit' : 4   # Retrieve at most 4 changes at once
+            })
+
+    for change in r['Changes']:
+        if change['ChangeType'] == 'StablePatient':
+            AnonymizePatient(change['Path'])
+
+    current = r['Last']
+
+    if r['Done']:
+        print('Everything has been processed: Waiting...')
+        time.sleep(1)
--- a/Resources/Samples/Python/DownloadAnonymized.py	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/Python/DownloadAnonymized.py	Thu May 21 16:58:30 2015 +0200
@@ -2,8 +2,8 @@
 # -*- coding: utf-8 -*-
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
-# Belgium
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -42,7 +42,7 @@
     if name.startswith('anonymized'):
 
         # Trigger the download
-        print 'Downloading %s' % name
+        print('Downloading %s' % name)
         zipContent = RestToolbox.DoGet('%s/patients/%s/archive' % (URL, patient))
         f = open(os.path.join('/tmp', name + '.zip'), 'wb')
         f.write(zipContent)
--- a/Resources/Samples/Python/HighPerformanceAutoRouting.py	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/Python/HighPerformanceAutoRouting.py	Thu May 21 16:58:30 2015 +0200
@@ -2,8 +2,8 @@
 # -*- coding: utf-8 -*-
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
-# Belgium
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -107,7 +107,7 @@
                 break
 
         if len(instances) > 0:
-            print 'Sending a packet of %d instances' % len(instances)
+            print('Sending a packet of %d instances' % len(instances))
             start = time.time()
 
             # Send all the instances with a single DICOM connexion
@@ -118,11 +118,13 @@
                 RestToolbox.DoDelete('%s/instances/%s' % (URL, instance))
 
             # Clear the log of the exported instances (to prevent the
-            # SQLite database from growing indefinitely)
+            # SQLite database from growing indefinitely). More simply,
+            # you could also set the "LogExportedResources" option to
+            # "false" in the configuration file since Orthanc 0.8.3.
             RestToolbox.DoDelete('%s/exports' % URL)
 
             end = time.time()
-            print 'The packet of %d instances has been sent in %d seconds' % (len(instances), end - start)
+            print('The packet of %d instances has been sent in %d seconds' % (len(instances), end - start))
 
 
 #
@@ -131,7 +133,7 @@
 
 def PrintProgress(queue):
     while True:
-        print 'Current queue size: %d' % (queue.qsize())
+        print('Current queue size: %d' % (queue.qsize()))
         time.sleep(1)
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/Python/Replicate.py	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,109 @@
+#!/usr/bin/python
+
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import base64
+import httplib2
+import json
+import re
+import sys
+
+URL_REGEX = re.compile('(http|https)://((.+?):(.+?)@|)(.*)')
+
+
+if len(sys.argv) != 3:
+    print("""
+Script to copy the content of one Orthanc server to another Orthanc
+server through their REST API.
+
+Usage: %s [SourceURI] [TargetURI]
+For instance: %s http://orthanc:password@localhost:8042/ http://localhost:8043/
+""" % (sys.argv[0], sys.argv[0]))
+    exit(-1)
+
+
+
+def CreateHeaders(parsedUrl):
+    headers = { }
+    username = parsedUrl.group(3)
+    password = parsedUrl.group(4)
+
+    if username != None and password != None:
+        # This is a custom reimplementation of the
+        # "Http.add_credentials()" method for Basic HTTP Access
+        # Authentication (for some weird reason, this method does not
+        # always work)
+        # http://en.wikipedia.org/wiki/Basic_access_authentication
+        headers['authorization'] = 'Basic ' + base64.b64encode(username + ':' + password)
+
+    return headers
+
+
+def GetBaseUrl(parsedUrl):
+    return '%s://%s' % (parsedUrl.group(1), parsedUrl.group(5))
+
+
+def DoGetString(url):
+    global URL_REGEX
+    parsedUrl = URL_REGEX.match(url)
+    headers = CreateHeaders(parsedUrl)
+
+    h = httplib2.Http()
+    resp, content = h.request(GetBaseUrl(parsedUrl), 'GET', headers = headers)
+
+    if resp.status == 200:
+        return content
+    else:
+        raise Exception('Unable to contact Orthanc at: ' + url)
+    
+
+def DoPostDicom(url, body):
+    global URL_REGEX
+    parsedUrl = URL_REGEX.match(url)
+    headers = CreateHeaders(parsedUrl)
+    headers['content-type'] = 'application/dicom'
+
+    h = httplib2.Http()
+    resp, content = h.request(GetBaseUrl(parsedUrl), 'POST',
+                              body = body,
+                              headers = headers)
+
+    if resp.status != 200:
+        raise Exception('Unable to contact Orthanc at: ' + url)
+    
+
+def _DecodeJson(s):
+    if (sys.version_info >= (3, 0)):
+        return json.loads(s.decode())
+    else:
+        return json.loads(s)
+
+
+def DoGetJson(url):
+    return _DecodeJson(DoGetString(url))
+
+
+SOURCE = sys.argv[1]
+TARGET = sys.argv[2]
+
+for study in DoGetJson('%s/studies' % SOURCE):
+    print('Sending study %s...' % study)
+    for instance in DoGetJson('%s/studies/%s/instances' % (SOURCE, study)):
+        dicom = DoGetString('%s/instances/%s/file' % (SOURCE, instance['ID']))
+        DoPostDicom('%s/instances' % TARGET, dicom)
--- a/Resources/Samples/Python/RestToolbox.py	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/Python/RestToolbox.py	Thu May 21 16:58:30 2015 +0200
@@ -1,6 +1,6 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
-# Belgium
+# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
 #
 # This program is free software: you can redistribute it and/or
 # modify it under the terms of the GNU General Public License as
@@ -18,10 +18,27 @@
 
 import httplib2
 import json
-from urllib import urlencode
+import sys
+
+if (sys.version_info >= (3, 0)):
+    from urllib.parse import urlencode
+else:
+    from urllib import urlencode
+
 
 _credentials = None
 
+
+def _DecodeJson(s):
+    try:
+        if (sys.version_info >= (3, 0)):
+            return json.loads(s.decode())
+        else:
+            return json.loads(s)
+    except:
+        return s
+
+
 def SetCredentials(username, password):
     global _credentials
     _credentials = (username, password)
@@ -42,12 +59,9 @@
     if not (resp.status in [ 200 ]):
         raise Exception(resp.status)
     elif not interpretAsJson:
-        return content
+        return content.decode()
     else:
-        try:
-            return json.loads(content)
-        except:
-            return content
+        return _DecodeJson(content)
 
 
 def _DoPutOrPost(uri, method, data, contentType):
@@ -72,10 +86,7 @@
     if not (resp.status in [ 200, 302 ]):
         raise Exception(resp.status)
     else:
-        try:
-            return json.loads(content)
-        except:
-            return content
+        return _DecodeJson(content)
 
 
 def DoDelete(uri):
@@ -86,10 +97,7 @@
     if not (resp.status in [ 200 ]):
         raise Exception(resp.status)
     else:
-        try:
-            return json.loads(content)
-        except:
-            return content
+        return _DecodeJson(content)
 
 
 def DoPut(uri, data = {}, contentType = ''):
--- a/Resources/Samples/Tools/CMakeLists.txt	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/Tools/CMakeLists.txt	Thu May 21 16:58:30 2015 +0200
@@ -20,6 +20,9 @@
 include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
 include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
 include(${ORTHANC_ROOT}/Resources/CMake/ZlibConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/GoogleLogConfiguration.cmake)
+
+include_directories(${ORTHANC_ROOT}/OrthancCppClient/SharedLibrary/Laaw)
 
 add_library(CommonLibraries
   ${BOOST_SOURCES}
@@ -27,8 +30,8 @@
   ${ORTHANC_ROOT}/Core/OrthancException.cpp
   ${ORTHANC_ROOT}/Core/Toolbox.cpp
   ${ORTHANC_ROOT}/Core/Uuid.cpp
-  ${ORTHANC_ROOT}/Resources/md5/md5.c
-  ${ORTHANC_ROOT}/Resources/base64/base64.cpp
+  ${ORTHANC_ROOT}/Resources/ThirdParty/md5/md5.c
+  ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp
   )
 
 add_executable(RecoverCompressedFile
@@ -37,4 +40,4 @@
   ${ORTHANC_ROOT}/Core/Compression/ZlibCompressor.cpp
   )
 
-target_link_libraries(RecoverCompressedFile CommonLibraries)
+target_link_libraries(RecoverCompressedFile CommonLibraries GoogleLog)
--- a/Resources/Samples/Tools/RecoverCompressedFile.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/Tools/RecoverCompressedFile.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/Resources/Samples/WebApplications/DrawingDicomizer.js	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/WebApplications/DrawingDicomizer.js	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * Permission is hereby granted, free of charge, to any person
  * obtaining a copy of this software and associated documentation
--- a/Resources/Samples/WebApplications/DrawingDicomizer/index.html	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/WebApplications/DrawingDicomizer/index.html	Thu May 21 16:58:30 2015 +0200
@@ -15,7 +15,16 @@
   <body>
     <canvas id="canvas" width="490" height="220"></canvas>
     <p>
-      Patient Name: <input type="text" id="patientName"></input>
+      Patient ID: <input type="text" id="patientID"></input>
+    </p>
+    <p>
+      Patient Name: <input type="text" id="patientName" value="HELLO^WORLD"></input>
+    </p>
+    <p>
+      Study Description: <input type="text" id="studyDescription" value="My Study"></input>
+    </p>
+    <p>
+      Series Description: <input type="text" id="seriesDescription" value="My Series"></input>
     </p>
     <p>
       <button id="submit">Submit</button>
--- a/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * Permission is hereby granted, free of charge, to any person
  * obtaining a copy of this software and associated documentation
@@ -24,23 +24,43 @@
  * SOFTWARE.
  **/
 
+function guid4Block() {
+  return Math.floor((1 + Math.random()) * 0x10000)
+    .toString(16)
+    .substring(1);
+}
+ 
+function guid() {
+  return (guid4Block() + guid4Block() + '-' + guid4Block() + '-' + guid4Block() + '-' +
+          guid4Block() + '-' + guid4Block() + guid4Block() + guid4Block());
+}
+
 
 $(document).ready(function() {
+  $('#patientID').val(guid());
+
   $('#submit').click(function(event) {
     var png = context.canvas.toDataURL();
 
     $.ajax({
       type: 'POST',
       url: '/orthanc/tools/create-dicom',
+      dataType: 'text',
       data: { 
+        PatientID: $('#patientID').val(),
         PatientName: $('#patientName').val(),
+        StudyDescription: $('#studyDescription').val(),
+        SeriesDescription: $('#seriesDescription').val(),
         PixelData: png,
-        Modality: 'RX' 
+        Modality: 'RX'
+      },
+      success : function(msg) {
+        alert('Your drawing has been DICOM-ized!\n\n' + msg);
+      },
+      error : function() {
+        alert('Error while DICOM-izing the drawing');
       }
-    })
-      .success(function( msg ) {
-        alert('Your drawing has been dicomized!\n\n' + msg);
-      });
+    });
 
     return false;
   });
--- a/Resources/Samples/WebApplications/NodeToolbox.js	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Samples/WebApplications/NodeToolbox.js	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * Permission is hereby granted, free of charge, to any person
  * obtaining a copy of this software and associated documentation
@@ -35,15 +35,24 @@
   opts.method = 'GET';
 
   http.get(opts, function(response) {
-    response.setEncoding('utf-8');
-    response.on('data', function(chunk) {
-      res.write(chunk);
-    });
-    response.on('end', function() {
+    if (response.statusCode == 200) {
+      response.setEncoding('utf-8');
+      response.on('data', function(chunk) {
+        res.write(chunk);
+      });
+      response.on('end', function() {
+        res.end();
+      });
+    } else {
+      console.log('Got error on GET forwarding: ' + 
+                  response.statusCode + ' (' + path + ')');
+      res.writeHead(response.statusCode);
       res.end();
-    });
+    }
   }).on('error', function(e) {
-    console.log('Got error on GET forwarding: ' + e.message + ' (' + path + ')');
+    console.log('Unable to contact Orthanc: ' + e.message);
+    res.writeHead(503);  // Service Unavailable
+    res.end();
   });
 }
 
@@ -57,15 +66,24 @@
   }
 
   var req = http.request(opts, function(response) {
-    response.setEncoding('utf-8');
-    response.on('data', function(chunk) {
-      res.write(chunk);
-    });
-    response.on('end', function() {
+    if (response.statusCode == 200) {
+      response.setEncoding('utf-8');
+      response.on('data', function(chunk) {
+        res.write(chunk);
+      });
+      response.on('end', function() {
+        res.end();
+      });
+    } else {
+      console.log('Got error on POST forwarding: ' + 
+                  response.statusCode + ' (' + path + ')');
+      res.writeHead(response.statusCode);
       res.end();
-    });
+    }
   }).on('error', function(e) {
-    console.log('Got error on POST forwarding: ' + e.message + ' (' + path + ')');
+    console.log('Unable to contact Orthanc: ' + e.message);
+    res.writeHead(503);  // Service Unavailable
+    res.end();
   });
 
   req.write(body);
--- a/Resources/Toolbox.lua	Wed Jun 25 15:34:40 2014 +0200
+++ b/Resources/Toolbox.lua	Thu May 21 16:58:30 2015 +0200
@@ -6,7 +6,8 @@
 --]]
 
 function PrintRecursive(s, l, i) -- recursive Print (structure, limit, indent)
-   l = (l) or 100; i = i or "";	-- default item limit, indent string
+   l = (l) or 100;  -- default item limit
+   i = i or "";     -- indent string
    if (l<1) then print "ERROR: Item limit reached."; return l-1 end;
    local ts = type(s);
    if (ts ~= "table") then print (i,ts,s); return l-1 end
@@ -18,4 +19,94 @@
    return l
 end	
 
+
+
+
+function _InitializeJob()
+   _job = {}
+end
+
+
+function _AccessJob()
+   return _job
+end
+
+
+function SendToModality(instanceId, modality)
+   if instanceId == nil then
+      error('Cannot send a nonexistent instance')
+   end
+
+   table.insert(_job, { 
+                   Operation = 'store-scu', 
+                   Instance = instanceId,
+                   Modality = modality 
+                })
+   return instanceId
+end
+
+
+function SendToPeer(instanceId, peer)
+   if instanceId == nil then
+      error('Cannot send a nonexistent instance')
+   end
+
+   table.insert(_job, { 
+                   Operation = 'store-peer', 
+                   Instance = instanceId,
+                   Peer = peer
+                })
+   return instanceId
+end
+
+
+function Delete(instanceId)
+   if instanceId == nil then
+      error('Cannot delete a nonexistent instance')
+   end
+
+   table.insert(_job, { 
+                   Operation = 'delete', 
+                   Instance = instanceId
+                })
+   return nil  -- Forbid chaining
+end
+
+
+function ModifyInstance(instanceId, replacements, removals, removePrivateTags)
+   if instanceId == nil then
+      error('Cannot modify a nonexistent instance')
+   end
+
+   if instanceId == '' then
+      error('Cannot modify twice an instance');
+   end
+
+   table.insert(_job, { 
+                   Operation = 'modify', 
+                   Instance = instanceId,
+                   Replace = replacements, 
+                   Remove = removals,
+                   RemovePrivateTags = removePrivateTags 
+                })
+   return ''  -- Chain with another operation
+end
+
+
+function CallSystem(instanceId, command, args)
+   if instanceId == nil then
+      error('Cannot modify a nonexistent instance')
+   end
+
+   table.insert(_job, { 
+                   Operation = 'call-system', 
+                   Instance = instanceId,
+                   Command = command,
+                   Arguments = args
+                })
+
+   return instanceId
+end
+
+
 print('Lua toolbox installed')
--- a/THANKS	Wed Jun 25 15:34:40 2014 +0200
+++ b/THANKS	Thu May 21 16:58:30 2015 +0200
@@ -8,8 +8,8 @@
 complete and exempt or errors.
 
 
-Code contributors
------------------
+Contributors
+------------
 
 * Cyril Paulus <cyril.paulus@student.ulg.ac.be>, for the build process
   and suggestions about the REST API.
@@ -20,6 +20,19 @@
 * 12maksqwe@gmail.com, for fixing issue #8.
 * Julien Nabet, for various suggestions to improve the source code.
 * Karsten Hilbert <Karsten.Hilbert@gmx.net>, for in-depth testing.
+* Marek Swiecicki <mswiecicki@archimedic.pl>, for various suggestions
+  and sample DICOM files.
+* Chris Hafey <chafey@gmail.com>, for suggesting many features and
+  improvements, for a Windows service+installer with .NET/NSIS.
+* Manabu Tokunaga <manabu@lury.net>, for a Windows service with .NET.
+* Vincent Kersten <vincent1234567@gmail.com>, for DICOMDIR in the GUI.
+* Emsy Chan <emlscs@yahoo.com>, for various contributions
+  and sample DICOM files.
+* Mikhail <mp39590@gmail.com>, for FreeBSD support.
+
+
+Thanks also to all the contributors active in our Google Group:
+https://groups.google.com/forum/#!forum/orthanc-users
 
 
 Debian/Ubuntu
--- a/UnitTestsSources/DicomMap.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,130 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Uuid.h"
-#include "../Core/OrthancException.h"
-#include "../Core/DicomFormat/DicomMap.h"
-#include "../Core/DicomFormat/DicomNullValue.h"
-
-#include <memory>
-
-using namespace Orthanc;
-
-TEST(DicomMap, MainTags)
-{
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient));
-  ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study));
-
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID));
-
-  std::set<DicomTag> s;
-  DicomMap::GetMainDicomTags(s);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
-
-  DicomMap::GetMainDicomTags(s, ResourceType_Patient);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID));
-
-  DicomMap::GetMainDicomTags(s, ResourceType_Study);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
-
-  DicomMap::GetMainDicomTags(s, ResourceType_Series);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
-
-  DicomMap::GetMainDicomTags(s, ResourceType_Instance);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
-}
-
-
-TEST(DicomMap, Tags)
-{
-  DicomMap m;
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_NAME));
-  ASSERT_FALSE(m.HasTag(0x0010, 0x0010));
-  m.SetValue(0x0010, 0x0010, "PatientName");
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_NAME));
-  ASSERT_TRUE(m.HasTag(0x0010, 0x0010));
-
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_ID));
-  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID");
-  ASSERT_TRUE(m.HasTag(0x0010, 0x0020));
-  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2");
-  ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).AsString());
-
-  m.Remove(DICOM_TAG_PATIENT_ID);
-  ASSERT_THROW(m.GetValue(0x0010, 0x0020), OrthancException);
-
-  std::auto_ptr<DicomMap> mm(m.Clone());
-  ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).AsString());  
-
-  m.SetValue(DICOM_TAG_PATIENT_ID, "Hello");
-  ASSERT_THROW(mm->GetValue(DICOM_TAG_PATIENT_ID), OrthancException);
-  mm->CopyTagIfExists(m, DICOM_TAG_PATIENT_ID);
-  ASSERT_EQ("Hello", mm->GetValue(DICOM_TAG_PATIENT_ID).AsString());  
-
-  DicomNullValue v;
-  ASSERT_TRUE(v.IsNull());
-}
-
-
-TEST(DicomMap, FindTemplates)
-{
-  DicomMap m;
-
-  DicomMap::SetupFindPatientTemplate(m);
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID));
-
-  DicomMap::SetupFindStudyTemplate(m);
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_ACCESSION_NUMBER));
-
-  DicomMap::SetupFindSeriesTemplate(m);
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID));
-
-  DicomMap::SetupFindInstanceTemplate(m);
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID));
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/DicomMapTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,206 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Uuid.h"
+#include "../Core/OrthancException.h"
+#include "../Core/DicomFormat/DicomMap.h"
+#include "../Core/DicomFormat/DicomNullValue.h"
+#include "../OrthancServer/FromDcmtkBridge.h"
+
+#include <memory>
+
+using namespace Orthanc;
+
+TEST(DicomMap, MainTags)
+{
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID));
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient));
+  ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study));
+
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER));
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID));
+
+  std::set<DicomTag> s;
+  DicomMap::GetMainDicomTags(s);
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
+
+  DicomMap::GetMainDicomTags(s, ResourceType_Patient);
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
+  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID));
+
+  DicomMap::GetMainDicomTags(s, ResourceType_Study);
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
+  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+
+  DicomMap::GetMainDicomTags(s, ResourceType_Series);
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
+  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+
+  DicomMap::GetMainDicomTags(s, ResourceType_Instance);
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
+  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+}
+
+
+TEST(DicomMap, Tags)
+{
+  std::set<DicomTag> s;
+
+  DicomMap m;
+  m.GetTags(s);
+  ASSERT_EQ(0, s.size());
+
+  ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_NAME));
+  ASSERT_FALSE(m.HasTag(0x0010, 0x0010));
+  m.SetValue(0x0010, 0x0010, "PatientName");
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(m.HasTag(0x0010, 0x0010));
+
+  m.GetTags(s);
+  ASSERT_EQ(1, s.size());
+  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin());
+
+  ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_ID));
+  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID");
+  ASSERT_TRUE(m.HasTag(0x0010, 0x0020));
+  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2");
+  ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).AsString());
+
+  m.GetTags(s);
+  ASSERT_EQ(2, s.size());
+
+  m.Remove(DICOM_TAG_PATIENT_ID);
+  ASSERT_THROW(m.GetValue(0x0010, 0x0020), OrthancException);
+
+  m.GetTags(s);
+  ASSERT_EQ(1, s.size());
+  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin());
+
+  std::auto_ptr<DicomMap> mm(m.Clone());
+  ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).AsString());  
+
+  m.SetValue(DICOM_TAG_PATIENT_ID, "Hello");
+  ASSERT_THROW(mm->GetValue(DICOM_TAG_PATIENT_ID), OrthancException);
+  mm->CopyTagIfExists(m, DICOM_TAG_PATIENT_ID);
+  ASSERT_EQ("Hello", mm->GetValue(DICOM_TAG_PATIENT_ID).AsString());  
+
+  DicomNullValue v;
+  ASSERT_TRUE(v.IsNull());
+}
+
+
+TEST(DicomMap, FindTemplates)
+{
+  DicomMap m;
+
+  DicomMap::SetupFindPatientTemplate(m);
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID));
+
+  DicomMap::SetupFindStudyTemplate(m);
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID));
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_ACCESSION_NUMBER));
+
+  DicomMap::SetupFindSeriesTemplate(m);
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID));
+
+  DicomMap::SetupFindInstanceTemplate(m);
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID));
+}
+
+
+
+
+static void TestModule(ResourceType level,
+                       DicomModule module)
+{
+  std::set<DicomTag> moduleTags, main;
+  DicomTag::GetTagsForModule(moduleTags, module);
+  DicomMap::GetMainDicomTags(main, level);
+  
+  // The main dicom tags are a subset of the module
+  for (std::set<DicomTag>::const_iterator it = main.begin(); it != main.end(); ++it)
+  {
+    bool ok = moduleTags.find(*it) != moduleTags.end();
+
+    // Exceptions for the Series level
+    /*if ((//
+          *it == DicomTag(0x, 0x) && 
+          level == ResourceType_Series))
+    {
+      ok = true;
+      }*/
+
+    // Exceptions for the Instance level
+    if ((/* Accession number, from Image module */
+          *it == DicomTag(0x0020, 0x0012) && 
+          level == ResourceType_Instance) ||
+        (/* Image Index, from PET Image module */
+          *it == DicomTag(0x0054, 0x1330) && 
+          level == ResourceType_Instance) ||
+        (/* Temporal Position Identifier, from MR Image module */
+          *it == DicomTag(0x0020, 0x0100) && 
+          level == ResourceType_Instance) ||
+        (/* Number of Frames, from Multi-frame module attributes, related to Image IOD */
+          *it == DicomTag(0x0028, 0x0008) && 
+          level == ResourceType_Instance ))
+    {
+      ok = true;
+    }
+
+    if (!ok)
+    {
+      std::cout << it->Format() << ": " << FromDcmtkBridge::GetName(*it)
+                << " not expected at level " << EnumerationToString(level) << std::endl;
+    }
+
+    EXPECT_TRUE(ok);
+  }
+}
+
+
+TEST(DicomMap, Modules)
+{
+  TestModule(ResourceType_Patient, DicomModule_Patient);
+  TestModule(ResourceType_Study, DicomModule_Study);
+  //TestModule(ResourceType_Series, DicomModule_Series);   // TODO
+  TestModule(ResourceType_Instance, DicomModule_Instance);
+}
--- a/UnitTestsSources/FileStorage.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,228 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include <ctype.h>
-#include <glog/logging.h>
-
-#include "../Core/FileStorage/FileStorage.h"
-#include "../OrthancServer/ServerIndex.h"
-#include "../Core/Toolbox.h"
-#include "../Core/OrthancException.h"
-#include "../Core/Uuid.h"
-#include "../Core/HttpServer/FilesystemHttpSender.h"
-#include "../Core/HttpServer/BufferHttpSender.h"
-#include "../Core/FileStorage/FileStorageAccessor.h"
-#include "../Core/FileStorage/CompressedFileStorageAccessor.h"
-
-using namespace Orthanc;
-
-
-static void StringToVector(std::vector<uint8_t>& v,
-                           const std::string& s)
-{
-  v.resize(s.size());
-  for (size_t i = 0; i < s.size(); i++)
-    v[i] = s[i];
-}
-
-
-TEST(FileStorage, Basic)
-{
-  FileStorage s("UnitTestsStorage");
-
-  std::string data = Toolbox::GenerateUuid();
-  std::string uid = s.Create(data);
-  std::string d;
-  s.ReadFile(d, uid);
-  ASSERT_EQ(d.size(), data.size());
-  ASSERT_FALSE(memcmp(&d[0], &data[0], data.size()));
-  ASSERT_EQ(s.GetCompressedSize(uid), data.size());
-}
-
-TEST(FileStorage, Basic2)
-{
-  FileStorage s("UnitTestsStorage");
-
-  std::vector<uint8_t> data;
-  StringToVector(data, Toolbox::GenerateUuid());
-  std::string uid = s.Create(data);
-  std::string d;
-  s.ReadFile(d, uid);
-  ASSERT_EQ(d.size(), data.size());
-  ASSERT_FALSE(memcmp(&d[0], &data[0], data.size()));
-  ASSERT_EQ(s.GetCompressedSize(uid), data.size());
-}
-
-TEST(FileStorage, EndToEnd)
-{
-  FileStorage s("UnitTestsStorage");
-  s.Clear();
-
-  std::list<std::string> u;
-  for (unsigned int i = 0; i < 10; i++)
-  {
-    u.push_back(s.Create(Toolbox::GenerateUuid()));
-  }
-
-  std::set<std::string> ss;
-  s.ListAllFiles(ss);
-  ASSERT_EQ(10u, ss.size());
-  
-  unsigned int c = 0;
-  for (std::list<std::string>::iterator
-         i = u.begin(); i != u.end(); i++, c++)
-  {
-    ASSERT_TRUE(ss.find(*i) != ss.end());
-    if (c < 5)
-      s.Remove(*i);
-  }
-
-  s.ListAllFiles(ss);
-  ASSERT_EQ(5u, ss.size());
-
-  s.Clear();
-  s.ListAllFiles(ss);
-  ASSERT_EQ(0u, ss.size());
-}
-
-
-TEST(FileStorageAccessor, Simple)
-{
-  FileStorage s("UnitTestsStorage");
-  FileStorageAccessor accessor(s);
-
-  std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_Dicom);
-  
-  std::string r;
-  accessor.Read(r, info.GetUuid());
-
-  ASSERT_EQ(data, r);
-  ASSERT_EQ(CompressionType_None, info.GetCompressionType());
-  ASSERT_EQ(11u, info.GetUncompressedSize());
-  ASSERT_EQ(11u, info.GetCompressedSize());
-  ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
-}
-
-
-TEST(FileStorageAccessor, NoCompression)
-{
-  FileStorage s("UnitTestsStorage");
-  CompressedFileStorageAccessor accessor(s);
-
-  accessor.SetCompressionForNextOperations(CompressionType_None);
-  std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_Dicom);
-  
-  std::string r;
-  accessor.Read(r, info.GetUuid());
-
-  ASSERT_EQ(data, r);
-  ASSERT_EQ(CompressionType_None, info.GetCompressionType());
-  ASSERT_EQ(11u, info.GetUncompressedSize());
-  ASSERT_EQ(11u, info.GetCompressedSize());
-  ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
-}
-
-
-TEST(FileStorageAccessor, NoCompression2)
-{
-  FileStorage s("UnitTestsStorage");
-  CompressedFileStorageAccessor accessor(s);
-
-  accessor.SetCompressionForNextOperations(CompressionType_None);
-  std::vector<uint8_t> data;
-  StringToVector(data, "Hello world");
-  FileInfo info = accessor.Write(data, FileContentType_Dicom);
-  
-  std::string r;
-  accessor.Read(r, info.GetUuid());
-
-  ASSERT_EQ(0, memcmp(&r[0], &data[0], data.size()));
-  ASSERT_EQ(CompressionType_None, info.GetCompressionType());
-  ASSERT_EQ(11u, info.GetUncompressedSize());
-  ASSERT_EQ(11u, info.GetCompressedSize());
-  ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
-}
-
-
-TEST(FileStorageAccessor, Compression)
-{
-  FileStorage s("UnitTestsStorage");
-  CompressedFileStorageAccessor accessor(s);
-
-  accessor.SetCompressionForNextOperations(CompressionType_Zlib);
-  std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_Dicom);
-  
-  std::string r;
-  accessor.Read(r, info.GetUuid());
-
-  ASSERT_EQ(data, r);
-  ASSERT_EQ(CompressionType_Zlib, info.GetCompressionType());
-  ASSERT_EQ(11u, info.GetUncompressedSize());
-  ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
-}
-
-
-TEST(FileStorageAccessor, Mix)
-{
-  FileStorage s("UnitTestsStorage");
-  CompressedFileStorageAccessor accessor(s);
-
-  std::string r;
-  std::string compressedData = "Hello";
-  std::string uncompressedData = "HelloWorld";
-
-  accessor.SetCompressionForNextOperations(CompressionType_Zlib);
-  FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom);
-  
-  accessor.SetCompressionForNextOperations(CompressionType_None);
-  FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom);
-  
-  accessor.SetCompressionForNextOperations(CompressionType_Zlib);
-  accessor.Read(r, compressedInfo.GetUuid());
-  ASSERT_EQ(compressedData, r);
-
-  accessor.SetCompressionForNextOperations(CompressionType_None);
-  accessor.Read(r, compressedInfo.GetUuid());
-  ASSERT_NE(compressedData, r);
-
-  /*
-  // This test is too slow on Windows
-  accessor.SetCompressionForNextOperations(CompressionType_Zlib);
-  ASSERT_THROW(accessor.Read(r, uncompressedInfo.GetUuid()), OrthancException);
-  */
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/FileStorageTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,233 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include <ctype.h>
+#include <glog/logging.h>
+
+#include "../Core/FileStorage/FilesystemStorage.h"
+#include "../OrthancServer/ServerIndex.h"
+#include "../Core/Toolbox.h"
+#include "../Core/OrthancException.h"
+#include "../Core/Uuid.h"
+#include "../Core/HttpServer/FilesystemHttpSender.h"
+#include "../Core/HttpServer/BufferHttpSender.h"
+#include "../Core/FileStorage/FileStorageAccessor.h"
+#include "../Core/FileStorage/CompressedFileStorageAccessor.h"
+
+using namespace Orthanc;
+
+
+static void StringToVector(std::vector<uint8_t>& v,
+                           const std::string& s)
+{
+  v.resize(s.size());
+  for (size_t i = 0; i < s.size(); i++)
+    v[i] = s[i];
+}
+
+
+TEST(FilesystemStorage, Basic)
+{
+  FilesystemStorage s("UnitTestsStorage");
+
+  std::string data = Toolbox::GenerateUuid();
+  std::string uid = Toolbox::GenerateUuid();
+  s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown);
+  std::string d;
+  s.Read(d, uid, FileContentType_Unknown);
+  ASSERT_EQ(d.size(), data.size());
+  ASSERT_FALSE(memcmp(&d[0], &data[0], data.size()));
+  ASSERT_EQ(s.GetSize(uid), data.size());
+}
+
+TEST(FilesystemStorage, Basic2)
+{
+  FilesystemStorage s("UnitTestsStorage");
+
+  std::vector<uint8_t> data;
+  StringToVector(data, Toolbox::GenerateUuid());
+  std::string uid = Toolbox::GenerateUuid();
+  s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown);
+  std::string d;
+  s.Read(d, uid, FileContentType_Unknown);
+  ASSERT_EQ(d.size(), data.size());
+  ASSERT_FALSE(memcmp(&d[0], &data[0], data.size()));
+  ASSERT_EQ(s.GetSize(uid), data.size());
+}
+
+TEST(FilesystemStorage, EndToEnd)
+{
+  FilesystemStorage s("UnitTestsStorage");
+  s.Clear();
+
+  std::list<std::string> u;
+  for (unsigned int i = 0; i < 10; i++)
+  {
+    std::string t = Toolbox::GenerateUuid();
+    std::string uid = Toolbox::GenerateUuid();
+    s.Create(uid.c_str(), &t[0], t.size(), FileContentType_Unknown);
+    u.push_back(uid);
+  }
+
+  std::set<std::string> ss;
+  s.ListAllFiles(ss);
+  ASSERT_EQ(10u, ss.size());
+  
+  unsigned int c = 0;
+  for (std::list<std::string>::iterator
+         i = u.begin(); i != u.end(); i++, c++)
+  {
+    ASSERT_TRUE(ss.find(*i) != ss.end());
+    if (c < 5)
+      s.Remove(*i, FileContentType_Unknown);
+  }
+
+  s.ListAllFiles(ss);
+  ASSERT_EQ(5u, ss.size());
+
+  s.Clear();
+  s.ListAllFiles(ss);
+  ASSERT_EQ(0u, ss.size());
+}
+
+
+TEST(FileStorageAccessor, Simple)
+{
+  FilesystemStorage s("UnitTestsStorage");
+  FileStorageAccessor accessor(s);
+
+  std::string data = "Hello world";
+  FileInfo info = accessor.Write(data, FileContentType_Dicom);
+  
+  std::string r;
+  accessor.Read(r, info.GetUuid(), FileContentType_Unknown);
+
+  ASSERT_EQ(data, r);
+  ASSERT_EQ(CompressionType_None, info.GetCompressionType());
+  ASSERT_EQ(11u, info.GetUncompressedSize());
+  ASSERT_EQ(11u, info.GetCompressedSize());
+  ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
+}
+
+
+TEST(FileStorageAccessor, NoCompression)
+{
+  FilesystemStorage s("UnitTestsStorage");
+  CompressedFileStorageAccessor accessor(s);
+
+  accessor.SetCompressionForNextOperations(CompressionType_None);
+  std::string data = "Hello world";
+  FileInfo info = accessor.Write(data, FileContentType_Dicom);
+  
+  std::string r;
+  accessor.Read(r, info.GetUuid(), FileContentType_Unknown);
+
+  ASSERT_EQ(data, r);
+  ASSERT_EQ(CompressionType_None, info.GetCompressionType());
+  ASSERT_EQ(11u, info.GetUncompressedSize());
+  ASSERT_EQ(11u, info.GetCompressedSize());
+  ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
+}
+
+
+TEST(FileStorageAccessor, NoCompression2)
+{
+  FilesystemStorage s("UnitTestsStorage");
+  CompressedFileStorageAccessor accessor(s);
+
+  accessor.SetCompressionForNextOperations(CompressionType_None);
+  std::vector<uint8_t> data;
+  StringToVector(data, "Hello world");
+  FileInfo info = accessor.Write(data, FileContentType_Dicom);
+  
+  std::string r;
+  accessor.Read(r, info.GetUuid(), FileContentType_Unknown);
+
+  ASSERT_EQ(0, memcmp(&r[0], &data[0], data.size()));
+  ASSERT_EQ(CompressionType_None, info.GetCompressionType());
+  ASSERT_EQ(11u, info.GetUncompressedSize());
+  ASSERT_EQ(11u, info.GetCompressedSize());
+  ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
+}
+
+
+TEST(FileStorageAccessor, Compression)
+{
+  FilesystemStorage s("UnitTestsStorage");
+  CompressedFileStorageAccessor accessor(s);
+
+  accessor.SetCompressionForNextOperations(CompressionType_Zlib);
+  std::string data = "Hello world";
+  FileInfo info = accessor.Write(data, FileContentType_Dicom);
+  
+  std::string r;
+  accessor.Read(r, info.GetUuid(), FileContentType_Unknown);
+
+  ASSERT_EQ(data, r);
+  ASSERT_EQ(CompressionType_Zlib, info.GetCompressionType());
+  ASSERT_EQ(11u, info.GetUncompressedSize());
+  ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
+}
+
+
+TEST(FileStorageAccessor, Mix)
+{
+  FilesystemStorage s("UnitTestsStorage");
+  CompressedFileStorageAccessor accessor(s);
+
+  std::string r;
+  std::string compressedData = "Hello";
+  std::string uncompressedData = "HelloWorld";
+
+  accessor.SetCompressionForNextOperations(CompressionType_Zlib);
+  FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom);
+  
+  accessor.SetCompressionForNextOperations(CompressionType_None);
+  FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom);
+  
+  accessor.SetCompressionForNextOperations(CompressionType_Zlib);
+  accessor.Read(r, compressedInfo.GetUuid(), FileContentType_Unknown);
+  ASSERT_EQ(compressedData, r);
+
+  accessor.SetCompressionForNextOperations(CompressionType_None);
+  accessor.Read(r, compressedInfo.GetUuid(), FileContentType_Unknown);
+  ASSERT_NE(compressedData, r);
+
+  /*
+  // This test is too slow on Windows
+  accessor.SetCompressionForNextOperations(CompressionType_Zlib);
+  ASSERT_THROW(accessor.Read(r, uncompressedInfo.GetUuid(), FileContentType_Unknown), OrthancException);
+  */
+}
--- a/UnitTestsSources/FromDcmtk.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,147 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../OrthancServer/FromDcmtkBridge.h"
-#include "../OrthancServer/OrthancInitialization.h"
-#include "../OrthancServer/DicomModification.h"
-#include "../Core/OrthancException.h"
-#include "../Core/ImageFormats/ImageBuffer.h"
-#include "../Core/ImageFormats/PngReader.h"
-#include "../Core/ImageFormats/PngWriter.h"
-
-using namespace Orthanc;
-
-TEST(DicomFormat, Tag)
-{
-  ASSERT_EQ("PatientName", FromDcmtkBridge::GetName(DicomTag(0x0010, 0x0010)));
-
-  DicomTag t = FromDcmtkBridge::ParseTag("SeriesDescription");
-  ASSERT_EQ(0x0008, t.GetGroup());
-  ASSERT_EQ(0x103E, t.GetElement());
-
-  t = FromDcmtkBridge::ParseTag("0020-e040");
-  ASSERT_EQ(0x0020, t.GetGroup());
-  ASSERT_EQ(0xe040, t.GetElement());
-
-  // Test ==() and !=() operators
-  ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020));
-  ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020));
-}
-
-
-TEST(DicomModification, Basic)
-{
-  DicomModification m;
-  m.SetupAnonymization();
-  //m.SetLevel(DicomRootLevel_Study);
-  //m.Replace(DICOM_TAG_PATIENT_ID, "coucou");
-  //m.Replace(DICOM_TAG_PATIENT_NAME, "coucou");
-
-  ParsedDicomFile o;
-  o.SaveToFile("UnitTestsResults/anon.dcm");
-
-  for (int i = 0; i < 10; i++)
-  {
-    char b[1024];
-    sprintf(b, "UnitTestsResults/anon%06d.dcm", i);
-    std::auto_ptr<ParsedDicomFile> f(o.Clone());
-    if (i > 4)
-      o.Replace(DICOM_TAG_SERIES_INSTANCE_UID, "coucou");
-    m.Apply(*f);
-    f->SaveToFile(b);
-  }
-}
-
-
-#include <dcmtk/dcmdata/dcuid.h>
-
-TEST(DicomModification, Png)
-{
-  // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image)
-  std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
-
-  std::string m, c;
-  Toolbox::DecodeDataUriScheme(m, c, s);
-
-  ASSERT_EQ("image/png", m);
-  ASSERT_EQ(116, c.size());
-
-  std::string cc;
-  Toolbox::DecodeBase64(cc, c);
-  PngReader reader;
-  reader.ReadFromMemory(cc);
-
-  ASSERT_EQ(5, reader.GetHeight());
-  ASSERT_EQ(5, reader.GetWidth());
-  ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat());
-
-  ParsedDicomFile o;
-  o.EmbedImage(s);
-  o.SaveToFile("UnitTestsResults/png1.dcm");
-
-  // Red dot, without alpha channel
-  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDTcIn2+8BgAAACJJREFUCNdj/P//PwMjIwME/P/P+J8BBTAxEOL/R9Lx/z8AynoKAXOeiV8AAAAASUVORK5CYII=";
-  o.EmbedImage(s);
-  o.SaveToFile("UnitTestsResults/png2.dcm");
-
-  // Check box in Graylevel8
-  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII=";
-  o.EmbedImage(s);
-  //o.Replace(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing);
-  o.SaveToFile("UnitTestsResults/png3.dcm");
-
-
-  {
-    // Gradient in Graylevel16
-
-    ImageBuffer img;
-    img.SetWidth(256);
-    img.SetHeight(256);
-    img.SetFormat(PixelFormat_Grayscale16);
-
-    int v = 0;
-    for (unsigned int y = 0; y < img.GetHeight(); y++)
-    {
-      uint16_t *p = reinterpret_cast<uint16_t*>(img.GetAccessor().GetRow(y));
-      for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++)
-      {
-        *p = v;
-      }
-    }
-
-    o.EmbedImage(img.GetAccessor());
-    o.SaveToFile("UnitTestsResults/png4.dcm");
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,296 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../OrthancServer/FromDcmtkBridge.h"
+#include "../OrthancServer/OrthancInitialization.h"
+#include "../OrthancServer/DicomModification.h"
+#include "../Core/OrthancException.h"
+#include "../Core/ImageFormats/ImageBuffer.h"
+#include "../Core/ImageFormats/PngReader.h"
+#include "../Core/ImageFormats/PngWriter.h"
+#include "../Core/Uuid.h"
+#include "../Resources/EncodingTests.h"
+
+using namespace Orthanc;
+
+TEST(DicomFormat, Tag)
+{
+  ASSERT_EQ("PatientName", FromDcmtkBridge::GetName(DicomTag(0x0010, 0x0010)));
+
+  DicomTag t = FromDcmtkBridge::ParseTag("SeriesDescription");
+  ASSERT_EQ(0x0008, t.GetGroup());
+  ASSERT_EQ(0x103E, t.GetElement());
+
+  t = FromDcmtkBridge::ParseTag("0020-e040");
+  ASSERT_EQ(0x0020, t.GetGroup());
+  ASSERT_EQ(0xe040, t.GetElement());
+
+  // Test ==() and !=() operators
+  ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020));
+  ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020));
+}
+
+
+TEST(DicomModification, Basic)
+{
+  DicomModification m;
+  m.SetupAnonymization();
+  //m.SetLevel(DicomRootLevel_Study);
+  //m.Replace(DICOM_TAG_PATIENT_ID, "coucou");
+  //m.Replace(DICOM_TAG_PATIENT_NAME, "coucou");
+
+  ParsedDicomFile o;
+  o.SaveToFile("UnitTestsResults/anon.dcm");
+
+  for (int i = 0; i < 10; i++)
+  {
+    char b[1024];
+    sprintf(b, "UnitTestsResults/anon%06d.dcm", i);
+    std::auto_ptr<ParsedDicomFile> f(o.Clone());
+    if (i > 4)
+      o.Replace(DICOM_TAG_SERIES_INSTANCE_UID, "coucou");
+    m.Apply(*f);
+    f->SaveToFile(b);
+  }
+}
+
+
+TEST(DicomModification, Anonymization)
+{
+  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, FromDcmtkBridge::ParseTag("PatientName"));
+
+  const DicomTag privateTag(0x0045, 0x0010);
+  const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020"));
+  ASSERT_TRUE(FromDcmtkBridge::IsPrivateTag(privateTag));
+  ASSERT_TRUE(FromDcmtkBridge::IsPrivateTag(privateTag2));
+  ASSERT_EQ(0x0031, privateTag2.GetGroup());
+  ASSERT_EQ(0x1020, privateTag2.GetElement());
+
+  std::string s;
+  ParsedDicomFile o;
+  o.Replace(DICOM_TAG_PATIENT_NAME, "coucou");
+  ASSERT_FALSE(o.GetTagValue(s, privateTag));
+  o.Insert(privateTag, "private tag");
+  ASSERT_TRUE(o.GetTagValue(s, privateTag));
+  ASSERT_STREQ("private tag", s.c_str());
+
+  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
+  ASSERT_THROW(o.Replace(privateTag2, "hello", DicomReplaceMode_ThrowIfAbsent), OrthancException);
+  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
+  o.Replace(privateTag2, "hello", DicomReplaceMode_IgnoreIfAbsent);
+  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
+  o.Replace(privateTag2, "hello", DicomReplaceMode_InsertIfAbsent);
+  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
+  ASSERT_STREQ("hello", s.c_str());
+  o.Replace(privateTag2, "hello world");
+  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
+  ASSERT_STREQ("hello world", s.c_str());
+
+  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_FALSE(Toolbox::IsUuid(s));
+
+  DicomModification m;
+  m.SetupAnonymization();
+  m.Keep(privateTag);
+
+  m.Apply(o);
+
+  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(Toolbox::IsUuid(s));
+  ASSERT_TRUE(o.GetTagValue(s, privateTag));
+  ASSERT_STREQ("private tag", s.c_str());
+  
+  m.SetupAnonymization();
+  m.Apply(o);
+  ASSERT_FALSE(o.GetTagValue(s, privateTag));
+}
+
+
+#include <dcmtk/dcmdata/dcuid.h>
+
+TEST(DicomModification, Png)
+{
+  // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image)
+  std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+  std::string m, c;
+  Toolbox::DecodeDataUriScheme(m, c, s);
+
+  ASSERT_EQ("image/png", m);
+  ASSERT_EQ(116, c.size());
+
+  std::string cc;
+  Toolbox::DecodeBase64(cc, c);
+  PngReader reader;
+  reader.ReadFromMemory(cc);
+
+  ASSERT_EQ(5, reader.GetHeight());
+  ASSERT_EQ(5, reader.GetWidth());
+  ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat());
+
+  ParsedDicomFile o;
+  o.EmbedImage(s);
+  o.SaveToFile("UnitTestsResults/png1.dcm");
+
+  // Red dot, without alpha channel
+  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDTcIn2+8BgAAACJJREFUCNdj/P//PwMjIwME/P/P+J8BBTAxEOL/R9Lx/z8AynoKAXOeiV8AAAAASUVORK5CYII=";
+  o.EmbedImage(s);
+  o.SaveToFile("UnitTestsResults/png2.dcm");
+
+  // Check box in Graylevel8
+  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII=";
+  o.EmbedImage(s);
+  //o.Replace(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing);
+  o.SaveToFile("UnitTestsResults/png3.dcm");
+
+
+  {
+    // Gradient in Graylevel16
+
+    ImageBuffer img;
+    img.SetWidth(256);
+    img.SetHeight(256);
+    img.SetFormat(PixelFormat_Grayscale16);
+
+    int v = 0;
+    for (unsigned int y = 0; y < img.GetHeight(); y++)
+    {
+      uint16_t *p = reinterpret_cast<uint16_t*>(img.GetAccessor().GetRow(y));
+      for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++)
+      {
+        *p = v;
+      }
+    }
+
+    o.EmbedImage(img.GetAccessor());
+    o.SaveToFile("UnitTestsResults/png4.dcm");
+  }
+}
+
+
+TEST(FromDcmtkBridge, Encodings1)
+{
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    std::string source(testEncodingsEncoded[i]);
+    std::string expected(testEncodingsExpected[i]);
+    std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i]);
+    std::cout << EnumerationToString(testEncodings[i]) << std::endl;
+    EXPECT_EQ(expected, s);
+  }
+}
+
+
+TEST(FromDcmtkBridge, Enumerations)
+{
+  Encoding e;
+
+  ASSERT_FALSE(GetDicomEncoding(e, ""));
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 6"));  ASSERT_EQ(Encoding_Utf8, e);
+
+  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-2
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 100"));  ASSERT_EQ(Encoding_Latin1, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 101"));  ASSERT_EQ(Encoding_Latin2, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 109"));  ASSERT_EQ(Encoding_Latin3, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 110"));  ASSERT_EQ(Encoding_Latin4, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 144"));  ASSERT_EQ(Encoding_Cyrillic, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 127"));  ASSERT_EQ(Encoding_Arabic, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 126"));  ASSERT_EQ(Encoding_Greek, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 138"));  ASSERT_EQ(Encoding_Hebrew, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 148"));  ASSERT_EQ(Encoding_Latin5, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 13"));  ASSERT_EQ(Encoding_Japanese, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 166"));  ASSERT_EQ(Encoding_Thai, e);
+
+  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-3
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 6"));  ASSERT_EQ(Encoding_Utf8, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 100"));  ASSERT_EQ(Encoding_Latin1, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 101"));  ASSERT_EQ(Encoding_Latin2, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 109"));  ASSERT_EQ(Encoding_Latin3, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 110"));  ASSERT_EQ(Encoding_Latin4, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 144"));  ASSERT_EQ(Encoding_Cyrillic, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 127"));  ASSERT_EQ(Encoding_Arabic, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 126"));  ASSERT_EQ(Encoding_Greek, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 138"));  ASSERT_EQ(Encoding_Hebrew, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 148"));  ASSERT_EQ(Encoding_Latin5, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 13"));  ASSERT_EQ(Encoding_Japanese, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 166"));  ASSERT_EQ(Encoding_Thai, e);
+
+  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-4
+  ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 87"));  //ASSERT_EQ(Encoding_JapaneseKanji, e);
+  ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 159"));  //ASSERT_EQ(Encoding_JapaneseKanjiSupplementary, e);
+  ASSERT_FALSE(GetDicomEncoding(e, "ISO 2022 IR 149"));  //ASSERT_EQ(Encoding_Korean, e);
+
+  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/ - Table C.12-5
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 192"));  ASSERT_EQ(Encoding_Utf8, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "GB18030")); ASSERT_EQ(Encoding_Chinese, e);
+}
+
+
+TEST(FromDcmtkBridge, Encodings3)
+{
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    std::cout << EnumerationToString(testEncodings[i]) << std::endl;
+    std::string dicom;
+
+    {
+      ParsedDicomFile f;
+      f.SetEncoding(testEncodings[i]);
+      f.Insert(DICOM_TAG_PATIENT_NAME, testEncodingsEncoded[i]);
+      f.SaveToMemoryBuffer(dicom);
+    }
+
+    if (testEncodings[i] != Encoding_Windows1251)
+    {
+      ParsedDicomFile g(dicom);
+
+      if (testEncodings[i] != Encoding_Ascii)
+      {
+        ASSERT_EQ(testEncodings[i], g.GetEncoding());
+      }
+
+      std::string tag;
+      ASSERT_TRUE(g.GetTagValue(tag, DICOM_TAG_PATIENT_NAME));
+      ASSERT_EQ(std::string(testEncodingsExpected[i]), tag);
+    }
+  }
+}
+
+
+TEST(FromDcmtkBridge, VR)
+{
+  ASSERT_TRUE(FromDcmtkBridge::IsPNValueRepresentation(DICOM_TAG_PATIENT_NAME));
+  ASSERT_FALSE(FromDcmtkBridge::IsPNValueRepresentation(DICOM_TAG_PATIENT_ID));
+}
--- a/UnitTestsSources/ImageProcessingTests.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/UnitTestsSources/ImageProcessingTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -51,6 +51,7 @@
   m.SetValue(DICOM_TAG_BITS_STORED, "12");
   m.SetValue(DICOM_TAG_HIGH_BIT, "11");
   m.SetValue(DICOM_TAG_PIXEL_REPRESENTATION, "0");
+  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
 
   DicomImageInformation info(m);
   PixelFormat format;
@@ -70,6 +71,7 @@
   m.SetValue(DICOM_TAG_BITS_STORED, "16");
   m.SetValue(DICOM_TAG_HIGH_BIT, "15");
   m.SetValue(DICOM_TAG_PIXEL_REPRESENTATION, "1");
+  m.SetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
 
   DicomImageInformation info(m);
   PixelFormat format;
--- a/UnitTestsSources/JpegLossless.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../OrthancServer/Internals/DicomImageDecoder.h"
-
-#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1
-
-#include <dcmtk/dcmdata/dcfilefo.h>
-
-#include "../OrthancServer/ParsedDicomFile.h"
-#include "../Core/OrthancException.h"
-#include "../Core/ImageFormats/ImageBuffer.h"
-#include "../Core/ImageFormats/PngWriter.h"
-
-using namespace Orthanc;
-
-
-
-// TODO Write a test
-
-
-#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/JpegLosslessTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../OrthancServer/Internals/DicomImageDecoder.h"
+
+#if ORTHANC_JPEG_LOSSLESS_ENABLED == 1
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+#include "../OrthancServer/ParsedDicomFile.h"
+#include "../Core/OrthancException.h"
+#include "../Core/ImageFormats/ImageBuffer.h"
+#include "../Core/ImageFormats/PngWriter.h"
+
+using namespace Orthanc;
+
+
+
+// TODO Write a test
+
+
+#endif
--- a/UnitTestsSources/Lua.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,136 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Lua/LuaFunctionCall.h"
-
-
-TEST(Lua, Json)
-{
-  Orthanc::LuaContext lua;
-  lua.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
-  lua.Execute("a={}");
-  lua.Execute("a['x'] = 10");
-  lua.Execute("a['y'] = {}");
-  lua.Execute("a['y'][1] = 20");
-  lua.Execute("a['y'][2] = 20");
-  lua.Execute("PrintRecursive(a)");
-
-  lua.Execute("function f(a) print(a.bool) return a.bool,20,30,40,50,60 end");
-
-  Json::Value v, vv, o;
-  //v["a"] = "b";
-  v.append("hello");
-  v.append("world");
-  v.append("42");
-  vv.append("sub");
-  vv.append("set");
-  v.append(vv);
-  o = Json::objectValue;
-  o["x"] = 10;
-  o["y"] = 20;
-  o["z"] = 20.5f;
-  v.append(o);
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
-    f.PushJSON(v);
-    f.Execute();
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "f");
-    f.PushJSON(o);
-    ASSERT_THROW(f.ExecutePredicate(), Orthanc::LuaException);
-  }
-
-  o["bool"] = false;
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "f");
-    f.PushJSON(o);
-    ASSERT_FALSE(f.ExecutePredicate());
-  }
-
-  o["bool"] = true;
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "f");
-    f.PushJSON(o);
-    ASSERT_TRUE(f.ExecutePredicate());
-  }
-}
-
-
-TEST(Lua, Existing)
-{
-  Orthanc::LuaContext lua;
-  lua.Execute("a={}");
-  lua.Execute("function f() end");
-
-  ASSERT_TRUE(lua.IsExistingFunction("f"));
-  ASSERT_FALSE(lua.IsExistingFunction("a"));
-  ASSERT_FALSE(lua.IsExistingFunction("Dummy"));
-}
-
-
-TEST(Lua, Simple)
-{
-  Orthanc::LuaContext lua;
-  lua.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
-    f.PushString("hello");
-    f.Execute();
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
-    f.PushBoolean(true);
-    f.Execute();
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
-    f.PushInteger(42);
-    f.Execute();
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
-    f.PushDouble(3.1415);
-    f.Execute();
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/LuaTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,290 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Toolbox.h"
+#include "../Core/Lua/LuaFunctionCall.h"
+
+#include <boost/lexical_cast.hpp>
+
+#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS)
+#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS"
+#endif
+
+
+TEST(Lua, Json)
+{
+  Orthanc::LuaContext lua;
+  lua.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
+  lua.Execute("a={}");
+  lua.Execute("a['x'] = 10");
+  lua.Execute("a['y'] = {}");
+  lua.Execute("a['y'][1] = 20");
+  lua.Execute("a['y'][2] = 20");
+  lua.Execute("PrintRecursive(a)");
+
+  lua.Execute("function f(a) print(a.bool) return a.bool,20,30,40,50,60 end");
+
+  Json::Value v, vv, o;
+  //v["a"] = "b";
+  v.append("hello");
+  v.append("world");
+  v.append("42");
+  vv.append("sub");
+  vv.append("set");
+  v.append(vv);
+  o = Json::objectValue;
+  o["x"] = 10;
+  o["y"] = 20;
+  o["z"] = 20.5f;
+  v.append(o);
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
+    f.PushJson(v);
+    f.Execute();
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "f");
+    f.PushJson(o);
+    ASSERT_THROW(f.ExecutePredicate(), Orthanc::LuaException);
+  }
+
+  o["bool"] = false;
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "f");
+    f.PushJson(o);
+    ASSERT_FALSE(f.ExecutePredicate());
+  }
+
+  o["bool"] = true;
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "f");
+    f.PushJson(o);
+    ASSERT_TRUE(f.ExecutePredicate());
+  }
+}
+
+
+TEST(Lua, Existing)
+{
+  Orthanc::LuaContext lua;
+  lua.Execute("a={}");
+  lua.Execute("function f() end");
+
+  ASSERT_TRUE(lua.IsExistingFunction("f"));
+  ASSERT_FALSE(lua.IsExistingFunction("a"));
+  ASSERT_FALSE(lua.IsExistingFunction("Dummy"));
+}
+
+
+TEST(Lua, Simple)
+{
+  Orthanc::LuaContext lua;
+  lua.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
+    f.PushString("hello");
+    f.Execute();
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
+    f.PushBoolean(true);
+    f.Execute();
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
+    f.PushInteger(42);
+    f.Execute();
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
+    f.PushDouble(3.1415);
+    f.Execute();
+  }
+}
+
+
+TEST(Lua, ReturnJson)
+{
+  Json::Value b = Json::objectValue;
+  b["a"] = 42;
+  b["b"] = 44;
+  b["c"] = 43;
+
+  Json::Value c = Json::arrayValue;
+  c.append("test3");
+  c.append("test1");
+  c.append("test2");
+
+  Json::Value a = Json::objectValue;
+  a["Hello"] = "World";
+  a["List"] = Json::arrayValue;
+  a["List"].append(b);
+  a["List"].append(c);
+
+  Orthanc::LuaContext lua;
+
+  // This is the identity function (it simply returns its input)
+  lua.Execute("function identity(a) return a end");
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson("hello");
+    Json::Value v;
+    f.ExecuteToJson(v);
+    ASSERT_EQ("hello", v.asString());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson(42.25);
+    Json::Value v;
+    f.ExecuteToJson(v);
+    ASSERT_FLOAT_EQ(42.25f, v.asFloat());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    Json::Value vv = Json::arrayValue;
+    f.PushJson(vv);
+    Json::Value v;
+    f.ExecuteToJson(v);
+    ASSERT_EQ(Json::arrayValue, v.type());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    Json::Value vv = Json::objectValue;
+    f.PushJson(vv);
+    Json::Value v;
+    f.ExecuteToJson(v);
+    // Lua does not make the distinction between empty lists and empty objects
+    ASSERT_EQ(Json::arrayValue, v.type());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson(b);
+    Json::Value v;
+    f.ExecuteToJson(v);
+    ASSERT_EQ(Json::objectValue, v.type());
+    ASSERT_FLOAT_EQ(42.0f, v["a"].asFloat());
+    ASSERT_FLOAT_EQ(44.0f, v["b"].asFloat());
+    ASSERT_FLOAT_EQ(43.0f, v["c"].asFloat());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson(c);
+    Json::Value v;
+    f.ExecuteToJson(v);
+    ASSERT_EQ(Json::arrayValue, v.type());
+    ASSERT_EQ("test3", v[0].asString());
+    ASSERT_EQ("test1", v[1].asString());
+    ASSERT_EQ("test2", v[2].asString());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson(a);
+    Json::Value v;
+    f.ExecuteToJson(v);
+    ASSERT_EQ("World", v["Hello"].asString());
+    ASSERT_EQ(42, v["List"][0]["a"].asInt());
+    ASSERT_EQ(44, v["List"][0]["b"].asInt());
+    ASSERT_EQ(43, v["List"][0]["c"].asInt());
+    ASSERT_EQ("test3", v["List"][1][0].asString());
+    ASSERT_EQ("test1", v["List"][1][1].asString());
+    ASSERT_EQ("test2", v["List"][1][2].asString());
+  }
+}
+
+TEST(Lua, Http)
+{
+  Orthanc::LuaContext lua;
+
+#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1  
+  lua.Execute("JSON = loadstring(HttpGet('http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/JSON.lua')) ()");
+  const std::string url("http://orthanc.googlecode.com/hg/OrthancCppClient/SharedLibrary/Product.json");
+#endif
+
+  std::string s;
+  lua.Execute(s, "print(HttpGet({}))");
+  ASSERT_EQ("ERROR", Orthanc::Toolbox::StripSpaces(s));
+
+#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1  
+  lua.Execute(s, "print(string.len(HttpGet(\"" + url + "\")))");
+  ASSERT_LE(100, boost::lexical_cast<int>(Orthanc::Toolbox::StripSpaces(s)));
+
+  // Parse a JSON file
+  lua.Execute(s, "print(JSON:decode(HttpGet(\"" + url + "\")) ['Product'])");
+  ASSERT_EQ("OrthancClient", Orthanc::Toolbox::StripSpaces(s));
+
+#if 0
+  // This part of the test can only be executed if one instance of
+  // Orthanc is running on the localhost
+
+  lua.Execute("modality = {}");
+  lua.Execute("table.insert(modality, 'ORTHANC')");
+  lua.Execute("table.insert(modality, 'localhost')");
+  lua.Execute("table.insert(modality, 4242)");
+  
+  lua.Execute(s, "print(HttpPost(\"http://localhost:8042/tools/execute-script\", \"print('hello world')\"))");
+  ASSERT_EQ("hello world", Orthanc::Toolbox::StripSpaces(s));
+
+  lua.Execute(s, "print(JSON:decode(HttpPost(\"http://localhost:8042/tools/execute-script\", \"print('[10,42,1000]')\")) [2])");
+  ASSERT_EQ("42", Orthanc::Toolbox::StripSpaces(s));
+
+  // Add/remove a modality with Lua
+  Json::Value v;
+  lua.Execute(s, "print(HttpGet('http://localhost:8042/modalities/lua'))");
+  ASSERT_EQ(0, Orthanc::Toolbox::StripSpaces(s).size());
+  lua.Execute(s, "print(HttpPut('http://localhost:8042/modalities/lua', JSON:encode(modality)))");
+  lua.Execute(v, "print(HttpGet('http://localhost:8042/modalities/lua'))");
+  ASSERT_TRUE(v.type() == Json::arrayValue);
+  lua.Execute(s, "print(HttpDelete('http://localhost:8042/modalities/lua'))");
+  lua.Execute(s, "print(HttpGet('http://localhost:8042/modalities/lua'))");
+  ASSERT_EQ(0, Orthanc::Toolbox::StripSpaces(s).size());
+#endif
+
+#endif
+}
--- a/UnitTestsSources/MemoryCache.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,230 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include <glog/logging.h>
-#include <memory>
-#include <boost/thread.hpp>
-#include <boost/lexical_cast.hpp>
-#include "../Core/IDynamicObject.h"
-#include "../Core/Cache/MemoryCache.h"
-
-
-TEST(LRU, Basic)
-{
-  Orthanc::LeastRecentlyUsedIndex<std::string> r;
-  
-  r.Add("d");
-  r.Add("a");
-  r.Add("c");
-  r.Add("b");
-
-  r.MakeMostRecent("a");
-  r.MakeMostRecent("d");
-  r.MakeMostRecent("b");
-  r.MakeMostRecent("c");
-  r.MakeMostRecent("d");
-  r.MakeMostRecent("c");
-
-  ASSERT_EQ("a", r.GetOldest());
-  ASSERT_EQ("a", r.RemoveOldest());
-  ASSERT_EQ("b", r.GetOldest());
-  ASSERT_EQ("b", r.RemoveOldest());
-  ASSERT_EQ("d", r.GetOldest());
-  ASSERT_EQ("d", r.RemoveOldest());
-  ASSERT_EQ("c", r.GetOldest());
-  ASSERT_EQ("c", r.RemoveOldest());
-
-  ASSERT_TRUE(r.IsEmpty());
-
-  ASSERT_THROW(r.GetOldest(), Orthanc::OrthancException);
-  ASSERT_THROW(r.RemoveOldest(), Orthanc::OrthancException);
-}
-
-
-TEST(LRU, Payload)
-{
-  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
-  
-  r.Add("a", 420);
-  r.Add("b", 421);
-  r.Add("c", 422);
-  r.Add("d", 423);
-
-  r.MakeMostRecent("a");
-  r.MakeMostRecent("d");
-  r.MakeMostRecent("b");
-  r.MakeMostRecent("c");
-  r.MakeMostRecent("d");
-  r.MakeMostRecent("c");
-
-  ASSERT_TRUE(r.Contains("b"));
-  ASSERT_EQ(421, r.Invalidate("b"));
-  ASSERT_FALSE(r.Contains("b"));
-
-  int p;
-  ASSERT_TRUE(r.Contains("a", p)); ASSERT_EQ(420, p);
-  ASSERT_TRUE(r.Contains("c", p)); ASSERT_EQ(422, p);
-  ASSERT_TRUE(r.Contains("d", p)); ASSERT_EQ(423, p);
-
-  ASSERT_EQ("a", r.GetOldest());
-  ASSERT_EQ(420, r.GetOldestPayload());
-  ASSERT_EQ("a", r.RemoveOldest(p)); ASSERT_EQ(420, p);
-
-  ASSERT_EQ("d", r.GetOldest());
-  ASSERT_EQ(423, r.GetOldestPayload());
-  ASSERT_EQ("d", r.RemoveOldest(p)); ASSERT_EQ(423, p);
-
-  ASSERT_EQ("c", r.GetOldest());
-  ASSERT_EQ(422, r.GetOldestPayload());
-  ASSERT_EQ("c", r.RemoveOldest(p)); ASSERT_EQ(422, p);
-
-  ASSERT_TRUE(r.IsEmpty());
-}
-
-
-TEST(LRU, PayloadUpdate)
-{
-  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
-  
-  r.Add("a", 420);
-  r.Add("b", 421);
-  r.Add("d", 423);
-
-  r.MakeMostRecent("a", 424);
-  r.MakeMostRecent("d", 421);
-
-  ASSERT_EQ("b", r.GetOldest());
-  ASSERT_EQ(421, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_EQ("a", r.GetOldest());
-  ASSERT_EQ(424, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_EQ("d", r.GetOldest());
-  ASSERT_EQ(421, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_TRUE(r.IsEmpty());
-}
-
-
-
-TEST(LRU, PayloadUpdateBis)
-{
-  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
-  
-  r.AddOrMakeMostRecent("a", 420);
-  r.AddOrMakeMostRecent("b", 421);
-  r.AddOrMakeMostRecent("d", 423);
-  r.AddOrMakeMostRecent("a", 424);
-  r.AddOrMakeMostRecent("d", 421);
-
-  ASSERT_EQ("b", r.GetOldest());
-  ASSERT_EQ(421, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_EQ("a", r.GetOldest());
-  ASSERT_EQ(424, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_EQ("d", r.GetOldest());
-  ASSERT_EQ(421, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_TRUE(r.IsEmpty());
-}
-
-
-
-
-namespace
-{
-  class Integer : public Orthanc::IDynamicObject
-  {
-  private:
-    std::string& log_;
-    int value_;
-
-  public:
-    Integer(std::string& log, int v) : log_(log), value_(v)
-    {
-    }
-
-    virtual ~Integer()
-    {
-      LOG(INFO) << "Removing cache entry for " << value_;
-      log_ += boost::lexical_cast<std::string>(value_) + " ";
-    }
-
-    int GetValue() const 
-    {
-      return value_;
-    }
-  };
-
-  class IntegerProvider : public Orthanc::ICachePageProvider
-  {
-  public:
-    std::string log_;
-
-    Orthanc::IDynamicObject* Provide(const std::string& s)
-    {
-      LOG(INFO) << "Providing " << s;
-      return new Integer(log_, boost::lexical_cast<int>(s));
-    }
-  };
-}
-
-
-TEST(MemoryCache, Basic)
-{
-  IntegerProvider provider;
-
-  {
-    Orthanc::MemoryCache cache(provider, 3);
-    cache.Access("42");  // 42 -> exit
-    cache.Access("43");  // 43, 42 -> exit
-    cache.Access("45");  // 45, 43, 42 -> exit
-    cache.Access("42");  // 42, 45, 43 -> exit
-    cache.Access("43");  // 43, 42, 45 -> exit
-    cache.Access("47");  // 45 is removed; 47, 43, 42 -> exit 
-    cache.Access("44");  // 42 is removed; 44, 47, 43 -> exit
-    cache.Access("42");  // 43 is removed; 42, 44, 47 -> exit
-    // Closing the cache: 47, 44, 42 are successively removed
-  }
-
-  ASSERT_EQ("45 42 43 47 44 42 ", provider.log_);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/MemoryCacheTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,230 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include <glog/logging.h>
+#include <memory>
+#include <boost/thread.hpp>
+#include <boost/lexical_cast.hpp>
+#include "../Core/IDynamicObject.h"
+#include "../Core/Cache/MemoryCache.h"
+
+
+TEST(LRU, Basic)
+{
+  Orthanc::LeastRecentlyUsedIndex<std::string> r;
+  
+  r.Add("d");
+  r.Add("a");
+  r.Add("c");
+  r.Add("b");
+
+  r.MakeMostRecent("a");
+  r.MakeMostRecent("d");
+  r.MakeMostRecent("b");
+  r.MakeMostRecent("c");
+  r.MakeMostRecent("d");
+  r.MakeMostRecent("c");
+
+  ASSERT_EQ("a", r.GetOldest());
+  ASSERT_EQ("a", r.RemoveOldest());
+  ASSERT_EQ("b", r.GetOldest());
+  ASSERT_EQ("b", r.RemoveOldest());
+  ASSERT_EQ("d", r.GetOldest());
+  ASSERT_EQ("d", r.RemoveOldest());
+  ASSERT_EQ("c", r.GetOldest());
+  ASSERT_EQ("c", r.RemoveOldest());
+
+  ASSERT_TRUE(r.IsEmpty());
+
+  ASSERT_THROW(r.GetOldest(), Orthanc::OrthancException);
+  ASSERT_THROW(r.RemoveOldest(), Orthanc::OrthancException);
+}
+
+
+TEST(LRU, Payload)
+{
+  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
+  
+  r.Add("a", 420);
+  r.Add("b", 421);
+  r.Add("c", 422);
+  r.Add("d", 423);
+
+  r.MakeMostRecent("a");
+  r.MakeMostRecent("d");
+  r.MakeMostRecent("b");
+  r.MakeMostRecent("c");
+  r.MakeMostRecent("d");
+  r.MakeMostRecent("c");
+
+  ASSERT_TRUE(r.Contains("b"));
+  ASSERT_EQ(421, r.Invalidate("b"));
+  ASSERT_FALSE(r.Contains("b"));
+
+  int p;
+  ASSERT_TRUE(r.Contains("a", p)); ASSERT_EQ(420, p);
+  ASSERT_TRUE(r.Contains("c", p)); ASSERT_EQ(422, p);
+  ASSERT_TRUE(r.Contains("d", p)); ASSERT_EQ(423, p);
+
+  ASSERT_EQ("a", r.GetOldest());
+  ASSERT_EQ(420, r.GetOldestPayload());
+  ASSERT_EQ("a", r.RemoveOldest(p)); ASSERT_EQ(420, p);
+
+  ASSERT_EQ("d", r.GetOldest());
+  ASSERT_EQ(423, r.GetOldestPayload());
+  ASSERT_EQ("d", r.RemoveOldest(p)); ASSERT_EQ(423, p);
+
+  ASSERT_EQ("c", r.GetOldest());
+  ASSERT_EQ(422, r.GetOldestPayload());
+  ASSERT_EQ("c", r.RemoveOldest(p)); ASSERT_EQ(422, p);
+
+  ASSERT_TRUE(r.IsEmpty());
+}
+
+
+TEST(LRU, PayloadUpdate)
+{
+  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
+  
+  r.Add("a", 420);
+  r.Add("b", 421);
+  r.Add("d", 423);
+
+  r.MakeMostRecent("a", 424);
+  r.MakeMostRecent("d", 421);
+
+  ASSERT_EQ("b", r.GetOldest());
+  ASSERT_EQ(421, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_EQ("a", r.GetOldest());
+  ASSERT_EQ(424, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_EQ("d", r.GetOldest());
+  ASSERT_EQ(421, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_TRUE(r.IsEmpty());
+}
+
+
+
+TEST(LRU, PayloadUpdateBis)
+{
+  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
+  
+  r.AddOrMakeMostRecent("a", 420);
+  r.AddOrMakeMostRecent("b", 421);
+  r.AddOrMakeMostRecent("d", 423);
+  r.AddOrMakeMostRecent("a", 424);
+  r.AddOrMakeMostRecent("d", 421);
+
+  ASSERT_EQ("b", r.GetOldest());
+  ASSERT_EQ(421, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_EQ("a", r.GetOldest());
+  ASSERT_EQ(424, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_EQ("d", r.GetOldest());
+  ASSERT_EQ(421, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_TRUE(r.IsEmpty());
+}
+
+
+
+
+namespace
+{
+  class Integer : public Orthanc::IDynamicObject
+  {
+  private:
+    std::string& log_;
+    int value_;
+
+  public:
+    Integer(std::string& log, int v) : log_(log), value_(v)
+    {
+    }
+
+    virtual ~Integer()
+    {
+      LOG(INFO) << "Removing cache entry for " << value_;
+      log_ += boost::lexical_cast<std::string>(value_) + " ";
+    }
+
+    int GetValue() const 
+    {
+      return value_;
+    }
+  };
+
+  class IntegerProvider : public Orthanc::ICachePageProvider
+  {
+  public:
+    std::string log_;
+
+    Orthanc::IDynamicObject* Provide(const std::string& s)
+    {
+      LOG(INFO) << "Providing " << s;
+      return new Integer(log_, boost::lexical_cast<int>(s));
+    }
+  };
+}
+
+
+TEST(MemoryCache, Basic)
+{
+  IntegerProvider provider;
+
+  {
+    Orthanc::MemoryCache cache(provider, 3);
+    cache.Access("42");  // 42 -> exit
+    cache.Access("43");  // 43, 42 -> exit
+    cache.Access("45");  // 45, 43, 42 -> exit
+    cache.Access("42");  // 42, 45, 43 -> exit
+    cache.Access("43");  // 43, 42, 45 -> exit
+    cache.Access("47");  // 45 is removed; 47, 43, 42 -> exit 
+    cache.Access("44");  // 42 is removed; 44, 47, 43 -> exit
+    cache.Access("42");  // 43 is removed; 42, 44, 47 -> exit
+    // Closing the cache: 47, 44, 42 are successively removed
+  }
+
+  ASSERT_EQ("45 42 43 47 44 42 ", provider.log_);
+}
--- a/UnitTestsSources/MultiThreading.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,276 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include <glog/logging.h>
-
-#include "../Core/OrthancException.h"
-#include "../Core/Toolbox.h"
-#include "../Core/MultiThreading/ArrayFilledByThreads.h"
-#include "../Core/MultiThreading/Locker.h"
-#include "../Core/MultiThreading/Mutex.h"
-#include "../Core/MultiThreading/ReaderWriterLock.h"
-#include "../Core/MultiThreading/ThreadedCommandProcessor.h"
-
-using namespace Orthanc;
-
-namespace
-{
-  class DynamicInteger : public ICommand
-  {
-  private:
-    int value_;
-    std::set<int>& target_;
-
-  public:
-    DynamicInteger(int value, std::set<int>& target) : 
-      value_(value), target_(target)
-    {
-    }
-
-    int GetValue() const
-    {
-      return value_;
-    }
-
-    virtual bool Execute()
-    {
-      static boost::mutex mutex;
-      boost::mutex::scoped_lock lock(mutex);
-      target_.insert(value_);
-      return true;
-    }
-  };
-
-  class MyFiller : public ArrayFilledByThreads::IFiller
-  {
-  private:
-    int size_;
-    unsigned int created_;
-    std::set<int> set_;
-
-  public:
-    MyFiller(int size) : size_(size), created_(0)
-    {
-    }
-
-    virtual size_t GetFillerSize()
-    {
-      return size_;
-    }
-
-    virtual IDynamicObject* GetFillerItem(size_t index)
-    {
-      static boost::mutex mutex;
-      boost::mutex::scoped_lock lock(mutex);
-      created_++;
-      return new DynamicInteger(index * 2, set_);
-    }
-
-    unsigned int GetCreatedCount() const
-    {
-      return created_;
-    }
-
-    std::set<int> GetSet()
-    {
-      return set_;
-    }    
-  };
-}
-
-
-
-
-TEST(MultiThreading, SharedMessageQueueBasic)
-{
-  std::set<int> s;
-
-  SharedMessageQueue q;
-  ASSERT_TRUE(q.WaitEmpty(0));
-  q.Enqueue(new DynamicInteger(10, s));
-  ASSERT_FALSE(q.WaitEmpty(1));
-  q.Enqueue(new DynamicInteger(20, s));
-  q.Enqueue(new DynamicInteger(30, s));
-  q.Enqueue(new DynamicInteger(40, s));
-
-  std::auto_ptr<DynamicInteger> i;
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(10, i->GetValue());
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(20, i->GetValue());
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(30, i->GetValue());
-  ASSERT_FALSE(q.WaitEmpty(1));
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(40, i->GetValue());
-  ASSERT_TRUE(q.WaitEmpty(0));
-  ASSERT_EQ(NULL, q.Dequeue(1));
-}
-
-
-TEST(MultiThreading, SharedMessageQueueClean)
-{
-  std::set<int> s;
-
-  try
-  {
-    SharedMessageQueue q;
-    q.Enqueue(new DynamicInteger(10, s));
-    q.Enqueue(new DynamicInteger(20, s));  
-    throw OrthancException("Nope");
-  }
-  catch (OrthancException&)
-  {
-  }
-}
-
-
-TEST(MultiThreading, ArrayFilledByThreadEmpty)
-{
-  MyFiller f(0);
-  ArrayFilledByThreads a(f);
-  a.SetThreadCount(1);
-  ASSERT_EQ(0, a.GetSize());
-}
-
-
-TEST(MultiThreading, ArrayFilledByThread1)
-{
-  MyFiller f(100);
-  ArrayFilledByThreads a(f);
-  a.SetThreadCount(1);
-  ASSERT_EQ(100, a.GetSize());
-  for (size_t i = 0; i < a.GetSize(); i++)
-  {
-    ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue());
-  }
-}
-
-
-TEST(MultiThreading, ArrayFilledByThread4)
-{
-  MyFiller f(100);
-  ArrayFilledByThreads a(f);
-  a.SetThreadCount(4);
-  ASSERT_EQ(100, a.GetSize());
-  for (size_t i = 0; i < a.GetSize(); i++)
-  {
-    ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue());
-  }
-
-  ASSERT_EQ(100u, f.GetCreatedCount());
-
-  a.Invalidate();
-
-  ASSERT_EQ(100, a.GetSize());
-  ASSERT_EQ(200u, f.GetCreatedCount());
-  ASSERT_EQ(4u, a.GetThreadCount());
-  ASSERT_TRUE(f.GetSet().empty());
-
-  for (size_t i = 0; i < a.GetSize(); i++)
-  {
-    ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue());
-  }
-}
-
-
-TEST(MultiThreading, CommandProcessor)
-{
-  ThreadedCommandProcessor p(4);
-
-  std::set<int> s;
-
-  for (size_t i = 0; i < 100; i++)
-  {
-    p.Post(new DynamicInteger(i * 2, s));
-  }
-
-  p.Join();
-
-  for (size_t i = 0; i < 200; i++)
-  {
-    if (i % 2)
-      ASSERT_TRUE(s.find(i) == s.end());
-    else
-      ASSERT_TRUE(s.find(i) != s.end());
-  }
-}
-
-
-TEST(MultiThreading, Mutex)
-{
-  Mutex mutex;
-  Locker locker(mutex);
-}
-
-
-TEST(MultiThreading, ReaderWriterLock)
-{
-  ReaderWriterLock lock;
-
-  {
-    Locker locker1(lock.ForReader());
-    Locker locker2(lock.ForReader());
-  }
-
-  {
-    Locker locker3(lock.ForWriter());
-  }
-}
-
-
-
-
-
-#include "../OrthancServer/DicomProtocol/ReusableDicomUserConnection.h"
-
-TEST(ReusableDicomUserConnection, DISABLED_Basic)
-{
-  ReusableDicomUserConnection c;
-  c.SetMillisecondsBeforeClose(200);
-  printf("START\n"); fflush(stdout);
-  {
-    ReusableDicomUserConnection::Locker lock(c, "STORESCP", "localhost", 2000, ModalityManufacturer_Generic);
-    lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676281");
-  }
-
-  printf("**\n"); fflush(stdout);
-  Toolbox::USleep(1000000);
-  printf("**\n"); fflush(stdout);
-
-  {
-    ReusableDicomUserConnection::Locker lock(c, "STORESCP", "localhost", 2000, ModalityManufacturer_Generic);
-    lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676277");
-  }
-
-  Toolbox::ServerBarrier();
-  printf("DONE\n"); fflush(stdout);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,374 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+#include <glog/logging.h>
+
+#include "../OrthancServer/Scheduler/ServerScheduler.h"
+
+#include "../Core/OrthancException.h"
+#include "../Core/Toolbox.h"
+#include "../Core/MultiThreading/ArrayFilledByThreads.h"
+#include "../Core/MultiThreading/Locker.h"
+#include "../Core/MultiThreading/Mutex.h"
+#include "../Core/MultiThreading/ReaderWriterLock.h"
+#include "../Core/MultiThreading/ThreadedCommandProcessor.h"
+
+using namespace Orthanc;
+
+namespace
+{
+  class DynamicInteger : public ICommand
+  {
+  private:
+    int value_;
+    std::set<int>& target_;
+
+  public:
+    DynamicInteger(int value, std::set<int>& target) : 
+      value_(value), target_(target)
+    {
+    }
+
+    int GetValue() const
+    {
+      return value_;
+    }
+
+    virtual bool Execute()
+    {
+      static boost::mutex mutex;
+      boost::mutex::scoped_lock lock(mutex);
+      target_.insert(value_);
+      return true;
+    }
+  };
+
+  class MyFiller : public ArrayFilledByThreads::IFiller
+  {
+  private:
+    int size_;
+    unsigned int created_;
+    std::set<int> set_;
+
+  public:
+    MyFiller(int size) : size_(size), created_(0)
+    {
+    }
+
+    virtual size_t GetFillerSize()
+    {
+      return size_;
+    }
+
+    virtual IDynamicObject* GetFillerItem(size_t index)
+    {
+      static boost::mutex mutex;
+      boost::mutex::scoped_lock lock(mutex);
+      created_++;
+      return new DynamicInteger(index * 2, set_);
+    }
+
+    unsigned int GetCreatedCount() const
+    {
+      return created_;
+    }
+
+    std::set<int> GetSet()
+    {
+      return set_;
+    }    
+  };
+}
+
+
+
+
+TEST(MultiThreading, SharedMessageQueueBasic)
+{
+  std::set<int> s;
+
+  SharedMessageQueue q;
+  ASSERT_TRUE(q.WaitEmpty(0));
+  q.Enqueue(new DynamicInteger(10, s));
+  ASSERT_FALSE(q.WaitEmpty(1));
+  q.Enqueue(new DynamicInteger(20, s));
+  q.Enqueue(new DynamicInteger(30, s));
+  q.Enqueue(new DynamicInteger(40, s));
+
+  std::auto_ptr<DynamicInteger> i;
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(10, i->GetValue());
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(20, i->GetValue());
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(30, i->GetValue());
+  ASSERT_FALSE(q.WaitEmpty(1));
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(40, i->GetValue());
+  ASSERT_TRUE(q.WaitEmpty(0));
+  ASSERT_EQ(NULL, q.Dequeue(1));
+}
+
+
+TEST(MultiThreading, SharedMessageQueueClean)
+{
+  std::set<int> s;
+
+  try
+  {
+    SharedMessageQueue q;
+    q.Enqueue(new DynamicInteger(10, s));
+    q.Enqueue(new DynamicInteger(20, s));  
+    throw OrthancException("Nope");
+  }
+  catch (OrthancException&)
+  {
+  }
+}
+
+
+TEST(MultiThreading, ArrayFilledByThreadEmpty)
+{
+  MyFiller f(0);
+  ArrayFilledByThreads a(f);
+  a.SetThreadCount(1);
+  ASSERT_EQ(0, a.GetSize());
+}
+
+
+TEST(MultiThreading, ArrayFilledByThread1)
+{
+  MyFiller f(100);
+  ArrayFilledByThreads a(f);
+  a.SetThreadCount(1);
+  ASSERT_EQ(100, a.GetSize());
+  for (size_t i = 0; i < a.GetSize(); i++)
+  {
+    ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue());
+  }
+}
+
+
+TEST(MultiThreading, ArrayFilledByThread4)
+{
+  MyFiller f(100);
+  ArrayFilledByThreads a(f);
+  a.SetThreadCount(4);
+  ASSERT_EQ(100, a.GetSize());
+  for (size_t i = 0; i < a.GetSize(); i++)
+  {
+    ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue());
+  }
+
+  ASSERT_EQ(100u, f.GetCreatedCount());
+
+  a.Invalidate();
+
+  ASSERT_EQ(100, a.GetSize());
+  ASSERT_EQ(200u, f.GetCreatedCount());
+  ASSERT_EQ(4u, a.GetThreadCount());
+  ASSERT_TRUE(f.GetSet().empty());
+
+  for (size_t i = 0; i < a.GetSize(); i++)
+  {
+    ASSERT_EQ(2 * i, dynamic_cast<DynamicInteger&>(a.GetItem(i)).GetValue());
+  }
+}
+
+
+TEST(MultiThreading, CommandProcessor)
+{
+  ThreadedCommandProcessor p(4);
+
+  std::set<int> s;
+
+  for (size_t i = 0; i < 100; i++)
+  {
+    p.Post(new DynamicInteger(i * 2, s));
+  }
+
+  p.Join();
+
+  for (size_t i = 0; i < 200; i++)
+  {
+    if (i % 2)
+      ASSERT_TRUE(s.find(i) == s.end());
+    else
+      ASSERT_TRUE(s.find(i) != s.end());
+  }
+}
+
+
+TEST(MultiThreading, Mutex)
+{
+  Mutex mutex;
+  Locker locker(mutex);
+}
+
+
+TEST(MultiThreading, ReaderWriterLock)
+{
+  ReaderWriterLock lock;
+
+  {
+    Locker locker1(lock.ForReader());
+    Locker locker2(lock.ForReader());
+  }
+
+  {
+    Locker locker3(lock.ForWriter());
+  }
+}
+
+
+
+#include "../OrthancServer/DicomProtocol/ReusableDicomUserConnection.h"
+
+TEST(ReusableDicomUserConnection, DISABLED_Basic)
+{
+  ReusableDicomUserConnection c;
+  c.SetMillisecondsBeforeClose(200);
+  printf("START\n"); fflush(stdout);
+
+  {
+    ReusableDicomUserConnection::Locker lock(c, "STORESCP", "localhost", 2000, ModalityManufacturer_Generic);
+    lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676281");
+  }
+
+  printf("**\n"); fflush(stdout);
+  Toolbox::USleep(1000000);
+  printf("**\n"); fflush(stdout);
+
+  {
+    ReusableDicomUserConnection::Locker lock(c, "STORESCP", "localhost", 2000, ModalityManufacturer_Generic);
+    lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676277");
+  }
+
+  Toolbox::ServerBarrier();
+  printf("DONE\n"); fflush(stdout);
+}
+
+
+
+class Tutu : public IServerCommand
+{
+private:
+  int factor_;
+
+public:
+  Tutu(int f) : factor_(f)
+  {
+  }
+
+  virtual bool Apply(ListOfStrings& outputs,
+                     const ListOfStrings& inputs)
+  {
+    for (ListOfStrings::const_iterator 
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      int a = boost::lexical_cast<int>(*it);
+      int b = factor_ * a;
+
+      printf("%d * %d = %d\n", a, factor_, b);
+
+      //if (a == 84) { printf("BREAK\n"); return false; }
+
+      outputs.push_back(boost::lexical_cast<std::string>(b));
+    }
+
+    Toolbox::USleep(100000);
+
+    return true;
+  }
+};
+
+
+static void Tata(ServerScheduler* s, ServerJob* j, bool* done)
+{
+  typedef IServerCommand::ListOfStrings  ListOfStrings;
+
+  while (!(*done))
+  {
+    ListOfStrings l;
+    s->GetListOfJobs(l);
+    for (ListOfStrings::iterator it = l.begin(); it != l.end(); ++it)
+    {
+      printf(">> %s: %0.1f\n", it->c_str(), 100.0f * s->GetProgress(*it));
+    }
+    Toolbox::USleep(10000);
+  }
+}
+
+
+TEST(MultiThreading, ServerScheduler)
+{
+  ServerScheduler scheduler(10);
+
+  ServerJob job;
+  ServerCommandInstance& f2 = job.AddCommand(new Tutu(2));
+  ServerCommandInstance& f3 = job.AddCommand(new Tutu(3));
+  ServerCommandInstance& f4 = job.AddCommand(new Tutu(4));
+  ServerCommandInstance& f5 = job.AddCommand(new Tutu(5));
+  f2.AddInput(boost::lexical_cast<std::string>(42));
+  //f3.AddInput(boost::lexical_cast<std::string>(42));
+  //f4.AddInput(boost::lexical_cast<std::string>(42));
+  f2.ConnectOutput(f3);
+  f3.ConnectOutput(f4);
+  f4.ConnectOutput(f5);
+
+  f3.SetConnectedToSink(true);
+  f5.SetConnectedToSink(true);
+
+  job.SetDescription("tutu");
+
+  bool done = false;
+  boost::thread t(Tata, &scheduler, &job, &done);
+
+
+  //scheduler.Submit(job);
+
+  IServerCommand::ListOfStrings l;
+  scheduler.SubmitAndWait(l, job);
+
+  ASSERT_EQ(2, l.size());
+  ASSERT_EQ(42 * 2 * 3, boost::lexical_cast<int>(l.front()));
+  ASSERT_EQ(42 * 2 * 3 * 4 * 5, boost::lexical_cast<int>(l.back()));
+
+  for (IServerCommand::ListOfStrings::iterator i = l.begin(); i != l.end(); i++)
+  {
+    printf("** %s\n", i->c_str());
+  }
+
+  //Toolbox::ServerBarrier();
+  //Toolbox::USleep(3000000);
+
+  done = true;
+  t.join();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/PluginsTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include <glog/logging.h>
+
+#include "../Plugins/Engine/PluginsManager.h"
+
+using namespace Orthanc;
+
+TEST(SharedLibrary, Basic)
+{
+#if defined(_WIN32)
+  SharedLibrary l("kernel32.dll");
+  ASSERT_THROW(l.GetFunction("world"), OrthancException);
+  ASSERT_TRUE(l.GetFunction("GetVersionExW") != NULL);
+  ASSERT_TRUE(l.HasFunction("GetVersionExW"));
+  ASSERT_FALSE(l.HasFunction("world"));
+
+#elif defined(__linux) || defined(__FreeBSD_kernel__)
+  SharedLibrary l("libdl.so");
+  ASSERT_THROW(l.GetFunction("world"), OrthancException);
+  ASSERT_TRUE(l.GetFunction("dlopen") != NULL);
+  ASSERT_TRUE(l.HasFunction("dlclose"));
+  ASSERT_FALSE(l.HasFunction("world"));
+
+#elif defined(__FreeBSD__)
+  // dlopen() in FreeBSD is supplied by libc, libc.so is
+  // a ldscript, so we can't actually use it. Use thread
+  // library instead - if it works - dlopen() is good.
+  SharedLibrary l("libpthread.so");
+  ASSERT_THROW(l.GetFunction("world"), OrthancException);
+  ASSERT_TRUE(l.GetFunction("pthread_create") != NULL);
+  ASSERT_TRUE(l.HasFunction("pthread_cancel"));
+  ASSERT_FALSE(l.HasFunction("world"));
+
+#elif defined(__APPLE__) && defined(__MACH__)
+  SharedLibrary l("libdl.dylib");
+  ASSERT_THROW(l.GetFunction("world"), OrthancException);
+  ASSERT_TRUE(l.GetFunction("dlopen") != NULL);
+  ASSERT_TRUE(l.HasFunction("dlclose"));
+  ASSERT_FALSE(l.HasFunction("world"));
+
+#else
+#error Support your platform here
+#endif
+}
--- a/UnitTestsSources/Png.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,186 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include <stdint.h>
-#include "../Core/ImageFormats/PngReader.h"
-#include "../Core/ImageFormats/PngWriter.h"
-#include "../Core/Toolbox.h"
-#include "../Core/Uuid.h"
-
-
-TEST(PngWriter, ColorPattern)
-{
-  Orthanc::PngWriter w;
-  int width = 17;
-  int height = 61;
-  int pitch = width * 3;
-
-  std::vector<uint8_t> image(height * pitch);
-  for (int y = 0; y < height; y++)
-  {
-    uint8_t *p = &image[0] + y * pitch;
-    for (int x = 0; x < width; x++, p += 3)
-    {
-      p[0] = (y % 3 == 0) ? 255 : 0;
-      p[1] = (y % 3 == 1) ? 255 : 0;
-      p[2] = (y % 3 == 2) ? 255 : 0;
-    }
-  }
-
-  w.WriteToFile("UnitTestsResults/ColorPattern.png", width, height, pitch, Orthanc::PixelFormat_RGB24, &image[0]);
-
-  std::string f, md5;
-  Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png");
-  Orthanc::Toolbox::ComputeMD5(md5, f);
-  ASSERT_EQ("604e785f53c99cae6ea4584870b2c41d", md5);
-}
-
-TEST(PngWriter, Gray8Pattern)
-{
-  Orthanc::PngWriter w;
-  int width = 17;
-  int height = 256;
-  int pitch = width;
-
-  std::vector<uint8_t> image(height * pitch);
-  for (int y = 0; y < height; y++)
-  {
-    uint8_t *p = &image[0] + y * pitch;
-    for (int x = 0; x < width; x++, p++)
-    {
-      *p = y;
-    }
-  }
-
-  w.WriteToFile("UnitTestsResults/Gray8Pattern.png", width, height, pitch, Orthanc::PixelFormat_Grayscale8, &image[0]);
-
-  std::string f, md5;
-  Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png");
-  Orthanc::Toolbox::ComputeMD5(md5, f);
-  ASSERT_EQ("5a9b98bea3d0a6d983980cc38bfbcdb3", md5);
-}
-
-TEST(PngWriter, Gray16Pattern)
-{
-  Orthanc::PngWriter w;
-  int width = 256;
-  int height = 256;
-  int pitch = width * 2 + 16;
-
-  std::vector<uint8_t> image(height * pitch);
-
-  int v = 0;
-  for (int y = 0; y < height; y++)
-  {
-    uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch);
-    for (int x = 0; x < width; x++, p++, v++)
-    {
-      *p = v;
-    }
-  }
-
-  w.WriteToFile("UnitTestsResults/Gray16Pattern.png", width, height, pitch, Orthanc::PixelFormat_Grayscale16, &image[0]);
-
-  std::string f, md5;
-  Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png");
-  Orthanc::Toolbox::ComputeMD5(md5, f);
-  ASSERT_EQ("0785866a08bf0a02d2eeff87f658571c", md5);
-}
-
-TEST(PngWriter, EndToEnd)
-{
-  Orthanc::PngWriter w;
-  int width = 256;
-  int height = 256;
-  int pitch = width * 2 + 16;
-
-  std::vector<uint8_t> image(height * pitch);
-
-  int v = 0;
-  for (int y = 0; y < height; y++)
-  {
-    uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch);
-    for (int x = 0; x < width; x++, p++, v++)
-    {
-      *p = v;
-    }
-  }
-
-  std::string s;
-  w.WriteToMemory(s, width, height, pitch, Orthanc::PixelFormat_Grayscale16, &image[0]);
-
-  {
-    Orthanc::PngReader r;
-    r.ReadFromMemory(s);
-
-    ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16);
-    ASSERT_EQ(r.GetWidth(), width);
-    ASSERT_EQ(r.GetHeight(), height);
-
-    v = 0;
-    for (int y = 0; y < height; y++)
-    {
-      const uint16_t *p = reinterpret_cast<const uint16_t*>((const uint8_t*) r.GetConstBuffer() + y * r.GetPitch());
-      ASSERT_EQ(p, r.GetConstRow(y));
-      for (int x = 0; x < width; x++, p++, v++)
-      {
-        ASSERT_EQ(*p, v);
-      }
-    }
-  }
-
-  {
-    Orthanc::Toolbox::TemporaryFile tmp;
-    Orthanc::Toolbox::WriteFile(s, tmp.GetPath());
-
-    Orthanc::PngReader r2;
-    r2.ReadFromFile(tmp.GetPath());
-
-    ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16);
-    ASSERT_EQ(r2.GetWidth(), width);
-    ASSERT_EQ(r2.GetHeight(), height);
-
-    v = 0;
-    for (int y = 0; y < height; y++)
-    {
-      const uint16_t *p = reinterpret_cast<const uint16_t*>((const uint8_t*) r2.GetConstBuffer() + y * r2.GetPitch());
-      ASSERT_EQ(p, r2.GetConstRow(y));
-      for (int x = 0; x < width; x++, p++, v++)
-      {
-        ASSERT_EQ(*p, v);
-      }
-    }
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/PngTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,186 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include <stdint.h>
+#include "../Core/ImageFormats/PngReader.h"
+#include "../Core/ImageFormats/PngWriter.h"
+#include "../Core/Toolbox.h"
+#include "../Core/Uuid.h"
+
+
+TEST(PngWriter, ColorPattern)
+{
+  Orthanc::PngWriter w;
+  int width = 17;
+  int height = 61;
+  int pitch = width * 3;
+
+  std::vector<uint8_t> image(height * pitch);
+  for (int y = 0; y < height; y++)
+  {
+    uint8_t *p = &image[0] + y * pitch;
+    for (int x = 0; x < width; x++, p += 3)
+    {
+      p[0] = (y % 3 == 0) ? 255 : 0;
+      p[1] = (y % 3 == 1) ? 255 : 0;
+      p[2] = (y % 3 == 2) ? 255 : 0;
+    }
+  }
+
+  w.WriteToFile("UnitTestsResults/ColorPattern.png", width, height, pitch, Orthanc::PixelFormat_RGB24, &image[0]);
+
+  std::string f, md5;
+  Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("604e785f53c99cae6ea4584870b2c41d", md5);
+}
+
+TEST(PngWriter, Gray8Pattern)
+{
+  Orthanc::PngWriter w;
+  int width = 17;
+  int height = 256;
+  int pitch = width;
+
+  std::vector<uint8_t> image(height * pitch);
+  for (int y = 0; y < height; y++)
+  {
+    uint8_t *p = &image[0] + y * pitch;
+    for (int x = 0; x < width; x++, p++)
+    {
+      *p = y;
+    }
+  }
+
+  w.WriteToFile("UnitTestsResults/Gray8Pattern.png", width, height, pitch, Orthanc::PixelFormat_Grayscale8, &image[0]);
+
+  std::string f, md5;
+  Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("5a9b98bea3d0a6d983980cc38bfbcdb3", md5);
+}
+
+TEST(PngWriter, Gray16Pattern)
+{
+  Orthanc::PngWriter w;
+  int width = 256;
+  int height = 256;
+  int pitch = width * 2 + 16;
+
+  std::vector<uint8_t> image(height * pitch);
+
+  int v = 0;
+  for (int y = 0; y < height; y++)
+  {
+    uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch);
+    for (int x = 0; x < width; x++, p++, v++)
+    {
+      *p = v;
+    }
+  }
+
+  w.WriteToFile("UnitTestsResults/Gray16Pattern.png", width, height, pitch, Orthanc::PixelFormat_Grayscale16, &image[0]);
+
+  std::string f, md5;
+  Orthanc::Toolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("0785866a08bf0a02d2eeff87f658571c", md5);
+}
+
+TEST(PngWriter, EndToEnd)
+{
+  Orthanc::PngWriter w;
+  int width = 256;
+  int height = 256;
+  int pitch = width * 2 + 16;
+
+  std::vector<uint8_t> image(height * pitch);
+
+  int v = 0;
+  for (int y = 0; y < height; y++)
+  {
+    uint16_t *p = reinterpret_cast<uint16_t*>(&image[0] + y * pitch);
+    for (int x = 0; x < width; x++, p++, v++)
+    {
+      *p = v;
+    }
+  }
+
+  std::string s;
+  w.WriteToMemory(s, width, height, pitch, Orthanc::PixelFormat_Grayscale16, &image[0]);
+
+  {
+    Orthanc::PngReader r;
+    r.ReadFromMemory(s);
+
+    ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16);
+    ASSERT_EQ(r.GetWidth(), width);
+    ASSERT_EQ(r.GetHeight(), height);
+
+    v = 0;
+    for (int y = 0; y < height; y++)
+    {
+      const uint16_t *p = reinterpret_cast<const uint16_t*>((const uint8_t*) r.GetConstBuffer() + y * r.GetPitch());
+      ASSERT_EQ(p, r.GetConstRow(y));
+      for (int x = 0; x < width; x++, p++, v++)
+      {
+        ASSERT_EQ(*p, v);
+      }
+    }
+  }
+
+  {
+    Orthanc::Toolbox::TemporaryFile tmp;
+    Orthanc::Toolbox::WriteFile(s, tmp.GetPath());
+
+    Orthanc::PngReader r2;
+    r2.ReadFromFile(tmp.GetPath());
+
+    ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16);
+    ASSERT_EQ(r2.GetWidth(), width);
+    ASSERT_EQ(r2.GetHeight(), height);
+
+    v = 0;
+    for (int y = 0; y < height; y++)
+    {
+      const uint16_t *p = reinterpret_cast<const uint16_t*>((const uint8_t*) r2.GetConstBuffer() + y * r2.GetPitch());
+      ASSERT_EQ(p, r2.GetConstRow(y));
+      for (int x = 0; x < width; x++, p++, v++)
+      {
+        ASSERT_EQ(*p, v);
+      }
+    }
+  }
+}
--- a/UnitTestsSources/PrecompiledHeadersUnitTests.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/UnitTestsSources/PrecompiledHeadersUnitTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/UnitTestsSources/PrecompiledHeadersUnitTests.h	Wed Jun 25 15:34:40 2014 +0200
+++ b/UnitTestsSources/PrecompiledHeadersUnitTests.h	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
--- a/UnitTestsSources/RestApi.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,161 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include <ctype.h>
-#include <glog/logging.h>
-
-#include "../Core/ChunkedBuffer.h"
-#include "../Core/HttpClient.h"
-#include "../Core/RestApi/RestApi.h"
-#include "../Core/Uuid.h"
-#include "../Core/OrthancException.h"
-#include "../Core/Compression/ZlibCompressor.h"
-
-using namespace Orthanc;
-
-#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS)
-#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS"
-#endif
-
-TEST(HttpClient, Basic)
-{
-  HttpClient c;
-  ASSERT_FALSE(c.IsVerbose());
-  c.SetVerbose(true);
-  ASSERT_TRUE(c.IsVerbose());
-  c.SetVerbose(false);
-  ASSERT_FALSE(c.IsVerbose());
-
-#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1
-  Json::Value v;
-  c.SetUrl("http://orthanc.googlecode.com/hg/Resources/Configuration.json");
-  c.Apply(v);
-  ASSERT_TRUE(v.isMember("StorageDirectory"));
-  //ASSERT_EQ(GetLastStatusText());
-
-  v = Json::nullValue;
-
-  HttpClient cc(c);
-  cc.SetUrl("https://orthanc.googlecode.com/hg/Resources/Configuration.json");
-  cc.Apply(v);
-  ASSERT_TRUE(v.isMember("LuaScripts"));
-#endif
-}
-
-TEST(RestApi, ChunkedBuffer)
-{
-  ChunkedBuffer b;
-  ASSERT_EQ(0, b.GetNumBytes());
-
-  b.AddChunk("hello", 5);
-  ASSERT_EQ(5, b.GetNumBytes());
-
-  b.AddChunk("world", 5);
-  ASSERT_EQ(10, b.GetNumBytes());
-
-  std::string s;
-  b.Flatten(s);
-  ASSERT_EQ("helloworld", s);
-}
-
-TEST(RestApi, ParseCookies)
-{
-  HttpHandler::Arguments headers;
-  HttpHandler::Arguments cookies;
-
-  headers["cookie"] = "a=b;c=d;;;e=f;;g=h;";
-  HttpHandler::ParseCookies(cookies, headers);
-  ASSERT_EQ(4u, cookies.size());
-  ASSERT_EQ("b", cookies["a"]);
-  ASSERT_EQ("d", cookies["c"]);
-  ASSERT_EQ("f", cookies["e"]);
-  ASSERT_EQ("h", cookies["g"]);
-
-  headers["cookie"] = "  name =  value  ; name2=value2";
-  HttpHandler::ParseCookies(cookies, headers);
-  ASSERT_EQ(2u, cookies.size());
-  ASSERT_EQ("value", cookies["name"]);
-  ASSERT_EQ("value2", cookies["name2"]);
-
-  headers["cookie"] = "  ;;;    ";
-  HttpHandler::ParseCookies(cookies, headers);
-  ASSERT_EQ(0u, cookies.size());
-
-  headers["cookie"] = "  ;   n=v  ;;    ";
-  HttpHandler::ParseCookies(cookies, headers);
-  ASSERT_EQ(1u, cookies.size());
-  ASSERT_EQ("v", cookies["n"]);
-}
-
-TEST(RestApi, RestApiPath)
-{
-  RestApiPath::Components args;
-  UriComponents trail;
-
-  {
-    RestApiPath uri("/coucou/{abc}/d/*");
-    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/e/f/g"));
-    ASSERT_EQ(1u, args.size());
-    ASSERT_EQ(3u, trail.size());
-    ASSERT_EQ("moi", args["abc"]);
-    ASSERT_EQ("e", trail[0]);
-    ASSERT_EQ("f", trail[1]);
-    ASSERT_EQ("g", trail[2]);
-
-    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/f"));
-    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/"));
-    ASSERT_FALSE(uri.Match(args, trail, "/a/moi/d"));
-    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi"));
-  }
-
-  {
-    RestApiPath uri("/coucou/{abc}/d");
-    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/d/e/f/g"));
-    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d"));
-    ASSERT_EQ(1u, args.size());
-    ASSERT_EQ(0u, trail.size());
-    ASSERT_EQ("moi", args["abc"]);
-  }
-
-  {
-    RestApiPath uri("/*");
-    ASSERT_TRUE(uri.Match(args, trail, "/a/b/c"));
-    ASSERT_EQ(0u, args.size());
-    ASSERT_EQ(3u, trail.size());
-    ASSERT_EQ("a", trail[0]);
-    ASSERT_EQ("b", trail[1]);
-    ASSERT_EQ("c", trail[2]);
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/RestApiTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,283 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include <ctype.h>
+#include <glog/logging.h>
+
+#include "../Core/ChunkedBuffer.h"
+#include "../Core/HttpClient.h"
+#include "../Core/RestApi/RestApi.h"
+#include "../Core/Uuid.h"
+#include "../Core/OrthancException.h"
+#include "../Core/Compression/ZlibCompressor.h"
+#include "../Core/RestApi/RestApiHierarchy.h"
+
+using namespace Orthanc;
+
+#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS)
+#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS"
+#endif
+
+TEST(HttpClient, Basic)
+{
+  HttpClient c;
+  ASSERT_FALSE(c.IsVerbose());
+  c.SetVerbose(true);
+  ASSERT_TRUE(c.IsVerbose());
+  c.SetVerbose(false);
+  ASSERT_FALSE(c.IsVerbose());
+
+#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1
+  Json::Value v;
+  c.SetUrl("http://orthanc.googlecode.com/hg/Resources/Configuration.json");
+  c.Apply(v);
+  ASSERT_TRUE(v.isMember("StorageDirectory"));
+  //ASSERT_EQ(GetLastStatusText());
+
+  v = Json::nullValue;
+
+  HttpClient cc(c);
+  cc.SetUrl("https://orthanc.googlecode.com/hg/Resources/Configuration.json");
+  cc.Apply(v);
+  ASSERT_TRUE(v.isMember("LuaScripts"));
+#endif
+}
+
+TEST(RestApi, ChunkedBuffer)
+{
+  ChunkedBuffer b;
+  ASSERT_EQ(0, b.GetNumBytes());
+
+  b.AddChunk("hello", 5);
+  ASSERT_EQ(5, b.GetNumBytes());
+
+  b.AddChunk("world", 5);
+  ASSERT_EQ(10, b.GetNumBytes());
+
+  std::string s;
+  b.Flatten(s);
+  ASSERT_EQ("helloworld", s);
+}
+
+TEST(RestApi, ParseCookies)
+{
+  HttpHandler::Arguments headers;
+  HttpHandler::Arguments cookies;
+
+  headers["cookie"] = "a=b;c=d;;;e=f;;g=h;";
+  HttpHandler::ParseCookies(cookies, headers);
+  ASSERT_EQ(4u, cookies.size());
+  ASSERT_EQ("b", cookies["a"]);
+  ASSERT_EQ("d", cookies["c"]);
+  ASSERT_EQ("f", cookies["e"]);
+  ASSERT_EQ("h", cookies["g"]);
+
+  headers["cookie"] = "  name =  value  ; name2=value2";
+  HttpHandler::ParseCookies(cookies, headers);
+  ASSERT_EQ(2u, cookies.size());
+  ASSERT_EQ("value", cookies["name"]);
+  ASSERT_EQ("value2", cookies["name2"]);
+
+  headers["cookie"] = "  ;;;    ";
+  HttpHandler::ParseCookies(cookies, headers);
+  ASSERT_EQ(0u, cookies.size());
+
+  headers["cookie"] = "  ;   n=v  ;;    ";
+  HttpHandler::ParseCookies(cookies, headers);
+  ASSERT_EQ(1u, cookies.size());
+  ASSERT_EQ("v", cookies["n"]);
+}
+
+TEST(RestApi, RestApiPath)
+{
+  HttpHandler::Arguments args;
+  UriComponents trail;
+
+  {
+    RestApiPath uri("/coucou/{abc}/d/*");
+    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/e/f/g"));
+    ASSERT_EQ(1u, args.size());
+    ASSERT_EQ(3u, trail.size());
+    ASSERT_EQ("moi", args["abc"]);
+    ASSERT_EQ("e", trail[0]);
+    ASSERT_EQ("f", trail[1]);
+    ASSERT_EQ("g", trail[2]);
+
+    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/f"));
+    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/"));
+    ASSERT_FALSE(uri.Match(args, trail, "/a/moi/d"));
+    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi"));
+
+    ASSERT_EQ(3u, uri.GetLevelCount());
+    ASSERT_TRUE(uri.IsUniversalTrailing());
+
+    ASSERT_EQ("coucou", uri.GetLevelName(0));
+    ASSERT_THROW(uri.GetWildcardName(0), OrthancException);
+
+    ASSERT_EQ("abc", uri.GetWildcardName(1));
+    ASSERT_THROW(uri.GetLevelName(1), OrthancException);
+
+    ASSERT_EQ("d", uri.GetLevelName(2));
+    ASSERT_THROW(uri.GetWildcardName(2), OrthancException);
+  }
+
+  {
+    RestApiPath uri("/coucou/{abc}/d");
+    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/d/e/f/g"));
+    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d"));
+    ASSERT_EQ(1u, args.size());
+    ASSERT_EQ(0u, trail.size());
+    ASSERT_EQ("moi", args["abc"]);
+
+    ASSERT_EQ(3u, uri.GetLevelCount());
+    ASSERT_FALSE(uri.IsUniversalTrailing());
+
+    ASSERT_EQ("coucou", uri.GetLevelName(0));
+    ASSERT_THROW(uri.GetWildcardName(0), OrthancException);
+
+    ASSERT_EQ("abc", uri.GetWildcardName(1));
+    ASSERT_THROW(uri.GetLevelName(1), OrthancException);
+
+    ASSERT_EQ("d", uri.GetLevelName(2));
+    ASSERT_THROW(uri.GetWildcardName(2), OrthancException);
+  }
+
+  {
+    RestApiPath uri("/*");
+    ASSERT_TRUE(uri.Match(args, trail, "/a/b/c"));
+    ASSERT_EQ(0u, args.size());
+    ASSERT_EQ(3u, trail.size());
+    ASSERT_EQ("a", trail[0]);
+    ASSERT_EQ("b", trail[1]);
+    ASSERT_EQ("c", trail[2]);
+
+    ASSERT_EQ(0u, uri.GetLevelCount());
+    ASSERT_TRUE(uri.IsUniversalTrailing());
+  }
+}
+
+
+
+
+
+
+static int testValue;
+
+template <int value>
+static void SetValue(RestApiGetCall& get)
+{
+  testValue = value;
+}
+
+
+static bool GetDirectory(Json::Value& target,
+                         RestApiHierarchy& hierarchy, 
+                         const std::string& uri)
+{
+  UriComponents p;
+  Toolbox::SplitUriComponents(p, uri);
+  return hierarchy.GetDirectory(target, p);
+}
+
+
+
+namespace
+{
+  class MyVisitor : public RestApiHierarchy::IVisitor
+  {
+  public:
+    virtual bool Visit(const RestApiHierarchy::Resource& resource,
+                       const UriComponents& uri,
+                       const HttpHandler::Arguments& components,
+                       const UriComponents& trailing)
+    {
+      return resource.Handle(*reinterpret_cast<RestApiGetCall*>(NULL));
+    }
+  };
+}
+
+
+static bool HandleGet(RestApiHierarchy& hierarchy, 
+                      const std::string& uri)
+{
+  UriComponents p;
+  Toolbox::SplitUriComponents(p, uri);
+  MyVisitor visitor;
+  return hierarchy.LookupResource(p, visitor);
+}
+
+
+TEST(RestApi, RestApiHierarchy)
+{
+  RestApiHierarchy root;
+  root.Register("/hello/world/test", SetValue<1>);
+  root.Register("/hello/world/test2", SetValue<2>);
+  root.Register("/hello/{world}/test3/test4", SetValue<3>);
+  root.Register("/hello2/*", SetValue<4>);
+
+  Json::Value m;
+  root.CreateSiteMap(m);
+  std::cout << m;
+
+  Json::Value d;
+  ASSERT_FALSE(GetDirectory(d, root, "/hello"));
+
+  ASSERT_TRUE(GetDirectory(d, root, "/hello/a")); 
+  ASSERT_EQ(1u, d.size());
+  ASSERT_EQ("test3", d[0].asString());
+
+  ASSERT_TRUE(GetDirectory(d, root, "/hello/world"));
+  ASSERT_EQ(2u, d.size());
+
+  ASSERT_TRUE(GetDirectory(d, root, "/hello/a/test3"));
+  ASSERT_EQ(1u, d.size());
+  ASSERT_EQ("test4", d[0].asString());
+
+  ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test"));
+  ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test2"));
+  ASSERT_FALSE(GetDirectory(d, root, "/hello2"));
+
+  testValue = 0;
+  ASSERT_TRUE(HandleGet(root, "/hello/world/test"));
+  ASSERT_EQ(testValue, 1);
+  ASSERT_TRUE(HandleGet(root, "/hello/world/test2"));
+  ASSERT_EQ(testValue, 2);
+  ASSERT_TRUE(HandleGet(root, "/hello/b/test3/test4"));
+  ASSERT_EQ(testValue, 3);
+  ASSERT_FALSE(HandleGet(root, "/hello/b/test3/test"));
+  ASSERT_EQ(testValue, 3);
+  ASSERT_TRUE(HandleGet(root, "/hello2/a/b"));
+  ASSERT_EQ(testValue, 4);
+}
--- a/UnitTestsSources/SQLite.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,334 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Toolbox.h"
-#include "../Core/SQLite/Connection.h"
-#include "../Core/SQLite/Statement.h"
-#include "../Core/SQLite/Transaction.h"
-
-#include <sqlite3.h>
-
-using namespace Orthanc;
-
-
-TEST(SQLite, Configuration)
-{
-  ASSERT_EQ(1, sqlite3_threadsafe());
-}
-
-
-TEST(SQLite, Connection)
-{
-  Toolbox::RemoveFile("UnitTestsResults/coucou");
-  SQLite::Connection c;
-  c.Open("UnitTestsResults/coucou");
-  c.Execute("CREATE TABLE c(k INTEGER PRIMARY KEY AUTOINCREMENT, v INTEGER)");
-  c.Execute("INSERT INTO c VALUES(NULL, 42);");
-}
-
-
-TEST(SQLite, StatementReferenceBasic)
-{
-  sqlite3* db;
-  sqlite3_open(":memory:", &db);
-
-  {
-    SQLite::StatementReference r(db, "SELECT * FROM sqlite_master");
-    ASSERT_EQ(0u, r.GetReferenceCount());
-
-    {
-      SQLite::StatementReference r1(r);
-      ASSERT_EQ(1u, r.GetReferenceCount());
-      ASSERT_EQ(0u, r1.GetReferenceCount());
-
-      {
-        SQLite::StatementReference r2(r);
-        ASSERT_EQ(2u, r.GetReferenceCount());
-        ASSERT_EQ(0u, r1.GetReferenceCount());
-        ASSERT_EQ(0u, r2.GetReferenceCount());
-
-        SQLite::StatementReference r3(r2);
-        ASSERT_EQ(3u, r.GetReferenceCount());
-        ASSERT_EQ(0u, r1.GetReferenceCount());
-        ASSERT_EQ(0u, r2.GetReferenceCount());
-        ASSERT_EQ(0u, r3.GetReferenceCount());
-      }
-
-      ASSERT_EQ(1u, r.GetReferenceCount());
-      ASSERT_EQ(0u, r1.GetReferenceCount());
-
-      {
-        SQLite::StatementReference r2(r);
-        ASSERT_EQ(2u, r.GetReferenceCount());
-        ASSERT_EQ(0u, r1.GetReferenceCount());
-        ASSERT_EQ(0u, r2.GetReferenceCount());
-      }
-
-      ASSERT_EQ(1u, r.GetReferenceCount());
-      ASSERT_EQ(0u, r1.GetReferenceCount());
-    }
-
-    ASSERT_EQ(0u, r.GetReferenceCount());
-  }
-
-  sqlite3_close(db);
-}
-
-TEST(SQLite, StatementBasic)
-{
-  SQLite::Connection c;
-  c.OpenInMemory();
-  
-  SQLite::Statement s(c, "SELECT * from sqlite_master");
-  s.Run();
-
-  for (unsigned int i = 0; i < 5; i++)
-  {
-    SQLite::Statement cs(c, SQLITE_FROM_HERE, "SELECT * from sqlite_master");
-    cs.Step();
-  }
-}
-
-
-namespace
-{
-  static bool destroyed;
-
-  class MyFunc : public SQLite::IScalarFunction
-  {
-  public:
-    MyFunc()
-    {
-      destroyed = false;
-    }
-
-    virtual ~MyFunc()
-    {
-      destroyed = true;
-    }
-
-    virtual const char* GetName() const
-    {
-      return "MYFUNC";
-    }
-
-    virtual unsigned int GetCardinality() const
-    {
-      return 2;
-    }
-
-    virtual void Compute(SQLite::FunctionContext& context)
-    {
-      context.SetIntResult(1000 + context.GetIntValue(0) * context.GetIntValue(1));
-    }
-  };
-
-  class MyDelete : public SQLite::IScalarFunction
-  {
-  public:
-    std::set<int> deleted_;
-
-    virtual const char* GetName() const
-    {
-      return "MYDELETE";
-    }
-
-    virtual unsigned int GetCardinality() const
-    {
-      return 1;
-    }
-
-    virtual void Compute(SQLite::FunctionContext& context)
-    {
-      deleted_.insert(context.GetIntValue(0));
-      context.SetNullResult();
-    }
-  };
-}
-
-TEST(SQLite, ScalarFunction)
-{
-  {
-    SQLite::Connection c;
-    c.OpenInMemory();
-    c.Register(new MyFunc());
-    c.Execute("CREATE TABLE t(id INTEGER PRIMARY KEY, v1 INTEGER, v2 INTEGER);");
-    c.Execute("INSERT INTO t VALUES(NULL, 2, 3);");
-    c.Execute("INSERT INTO t VALUES(NULL, 4, 4);");
-    c.Execute("INSERT INTO t VALUES(NULL, 6, 5);");
-    SQLite::Statement t(c, "SELECT MYFUNC(v1, v2), v1, v2 FROM t");
-    int i = 0;
-    while (t.Step())
-    {
-      ASSERT_EQ(t.ColumnInt(0), 1000 + t.ColumnInt(1) * t.ColumnInt(2));
-      i++;
-    }
-    ASSERT_EQ(3, i);
-    ASSERT_FALSE(destroyed);
-  }
-  ASSERT_TRUE(destroyed);
-}
-
-TEST(SQLite, CascadedDeleteCallback)
-{
-  SQLite::Connection c;
-  c.OpenInMemory();
-  MyDelete *func = new MyDelete();
-  c.Register(func);
-  c.Execute("CREATE TABLE parent(id INTEGER PRIMARY KEY, dummy INTEGER);");
-  c.Execute("CREATE TABLE child("
-            "  id INTEGER PRIMARY KEY, "
-            "  parent INTEGER REFERENCES parent(id) ON DELETE CASCADE, "
-            "  value INTEGER);");
-  c.Execute("CREATE TRIGGER childRemoved "
-            "AFTER DELETE ON child "
-            "FOR EACH ROW BEGIN "
-            "  SELECT MYDELETE(old.value); "
-            "END;");
-
-  c.Execute("INSERT INTO parent VALUES(42, 100);");
-  c.Execute("INSERT INTO parent VALUES(43, 101);");
-
-  c.Execute("INSERT INTO child VALUES(NULL, 42, 4200);");
-  c.Execute("INSERT INTO child VALUES(NULL, 42, 4201);");
-
-  c.Execute("INSERT INTO child VALUES(NULL, 43, 4300);");
-  c.Execute("INSERT INTO child VALUES(NULL, 43, 4301);");
-
-  // The following command deletes "parent(43, 101)", then in turns
-  // "child(NULL, 43, 4300/4301)", then calls the MyDelete on 4300 and
-  // 4301
-  c.Execute("DELETE FROM parent WHERE dummy=101");
-
-  ASSERT_EQ(2u, func->deleted_.size());
-  ASSERT_TRUE(func->deleted_.find(4300) != func->deleted_.end());
-  ASSERT_TRUE(func->deleted_.find(4301) != func->deleted_.end());
-}
-
-
-TEST(SQLite, EmptyTransactions)
-{
-  try
-  {
-    SQLite::Connection c;
-    c.OpenInMemory();
-
-    c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY);");
-    c.Execute("INSERT INTO a VALUES(NULL)");
-      
-    {
-      SQLite::Transaction t(c);
-      t.Begin();
-      {
-        SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a");
-        s.Step();
-      }
-      //t.Commit();
-    }
-
-    {
-      SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a");
-      s.Step();
-    }
-  }
-  catch (OrthancException& e)
-  {
-    fprintf(stderr, "Exception: [%s]\n", e.What());
-    throw e;
-  }
-}
-
-
-TEST(SQLite, Types)
-{
-  SQLite::Connection c;
-  c.OpenInMemory();
-  c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY, value)");
-
-  {
-    SQLite::Statement s(c, std::string("SELECT * FROM a"));
-    ASSERT_EQ(2, s.ColumnCount());
-    ASSERT_FALSE(s.Step());
-  }
-
-  {
-    SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a"));
-    ASSERT_FALSE(s.Step());
-    ASSERT_EQ("SELECT * FROM a", s.GetOriginalSQLStatement());
-  }
-
-  {
-    SQLite::Statement s(c, SQLITE_FROM_HERE, "INSERT INTO a VALUES(NULL, ?);");
-    s.BindNull(0);             ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindBool(0, true);       ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindInt(0, 42);          ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindInt64(0, 42ll);      ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindDouble(0, 42.5);     ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindCString(0, "Hello"); ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindBlob(0, "Hello", 5); ASSERT_TRUE(s.Run()); s.Reset();
-  }
-
-  {
-    SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a"));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_NULL, s.GetColumnType(1));
-    ASSERT_TRUE(s.ColumnIsNull(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
-    ASSERT_TRUE(s.ColumnBool(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
-    ASSERT_EQ(42, s.ColumnInt(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
-    ASSERT_EQ(42ll, s.ColumnInt64(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_FLOAT, s.GetColumnType(1));
-    ASSERT_DOUBLE_EQ(42.5, s.ColumnDouble(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_TEXT, s.GetColumnType(1));
-    ASSERT_EQ("Hello", s.ColumnString(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_BLOB, s.GetColumnType(1));
-    ASSERT_EQ(5, s.ColumnByteLength(1));
-    ASSERT_TRUE(!memcmp("Hello", s.ColumnBlob(1), 5));
-
-    std::string t;
-    ASSERT_TRUE(s.ColumnBlobAsString(1, &t));
-    ASSERT_EQ("Hello", t);
-
-    ASSERT_FALSE(s.Step());
-  }
-}
--- a/UnitTestsSources/SQLiteChromium.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,377 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Toolbox.h"
-#include "../Core/SQLite/Connection.h"
-#include "../Core/SQLite/Statement.h"
-#include "../Core/SQLite/Transaction.h"
-
-#include <sqlite3.h>
-
-
-using namespace Orthanc;
-using namespace Orthanc::SQLite;
-
-
-/********************************************************************
- ** Tests from
- ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/connection_unittest.cc
- ********************************************************************/
-
-class SQLConnectionTest : public testing::Test 
-{
-public:
-  SQLConnectionTest()
-  {
-  }
-
-  virtual ~SQLConnectionTest()
-  {
-  }
-
-  virtual void SetUp() 
-  {
-    db_.OpenInMemory();
-  }
-
-  virtual void TearDown() 
-  {
-    db_.Close();
-  }
-
-  Connection& db() 
-  { 
-    return db_; 
-  }
-
-private:
-  Connection db_;
-};
-
-
-
-TEST_F(SQLConnectionTest, Execute) 
-{
-  // Valid statement should return true.
-  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  EXPECT_EQ(SQLITE_OK, db().GetErrorCode());
-
-  // Invalid statement should fail.
-  ASSERT_EQ(SQLITE_ERROR,
-            db().ExecuteAndReturnErrorCode("CREATE TAB foo (a, b"));
-  EXPECT_EQ(SQLITE_ERROR, db().GetErrorCode());
-}
-
-TEST_F(SQLConnectionTest, ExecuteWithErrorCode) {
-  ASSERT_EQ(SQLITE_OK,
-            db().ExecuteAndReturnErrorCode("CREATE TABLE foo (a, b)"));
-  ASSERT_EQ(SQLITE_ERROR,
-            db().ExecuteAndReturnErrorCode("CREATE TABLE TABLE"));
-  ASSERT_EQ(SQLITE_ERROR,
-            db().ExecuteAndReturnErrorCode(
-              "INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)"));
-}
-
-TEST_F(SQLConnectionTest, CachedStatement) {
-  StatementId id1("foo", 12);
-  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  ASSERT_TRUE(db().Execute("INSERT INTO foo(a, b) VALUES (12, 13)"));
-
-  // Create a new cached statement.
-  {
-    Statement s(db(), id1, "SELECT a FROM foo");
-    ASSERT_TRUE(s.Step());
-    EXPECT_EQ(12, s.ColumnInt(0));
-  }
-
-  // The statement should be cached still.
-  EXPECT_TRUE(db().HasCachedStatement(id1));
-
-  {
-    // Get the same statement using different SQL. This should ignore our
-    // SQL and use the cached one (so it will be valid).
-    Statement s(db(), id1, "something invalid(");
-    ASSERT_TRUE(s.Step());
-    EXPECT_EQ(12, s.ColumnInt(0));
-  }
-
-  // Make sure other statements aren't marked as cached.
-  EXPECT_FALSE(db().HasCachedStatement(SQLITE_FROM_HERE));
-}
-
-TEST_F(SQLConnectionTest, IsSQLValidTest) {
-  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  ASSERT_TRUE(db().IsSQLValid("SELECT a FROM foo"));
-  ASSERT_FALSE(db().IsSQLValid("SELECT no_exist FROM foo"));
-}
-
-
-
-TEST_F(SQLConnectionTest, DoesStuffExist) {
-  // Test DoesTableExist.
-  EXPECT_FALSE(db().DoesTableExist("foo"));
-  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  EXPECT_TRUE(db().DoesTableExist("foo"));
-
-  // Should be case sensitive.
-  EXPECT_FALSE(db().DoesTableExist("FOO"));
-
-  // Test DoesColumnExist.
-  EXPECT_FALSE(db().DoesColumnExist("foo", "bar"));
-  EXPECT_TRUE(db().DoesColumnExist("foo", "a"));
-
-  // Testing for a column on a nonexistent table.
-  EXPECT_FALSE(db().DoesColumnExist("bar", "b"));
-}
-
-TEST_F(SQLConnectionTest, GetLastInsertRowId) {
-  ASSERT_TRUE(db().Execute("CREATE TABLE foo (id INTEGER PRIMARY KEY, value)"));
-
-  ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)"));
-
-  // Last insert row ID should be valid.
-  int64_t row = db().GetLastInsertRowId();
-  EXPECT_LT(0, row);
-
-  // It should be the primary key of the row we just inserted.
-  Statement s(db(), "SELECT value FROM foo WHERE id=?");
-  s.BindInt64(0, row);
-  ASSERT_TRUE(s.Step());
-  EXPECT_EQ(12, s.ColumnInt(0));
-}
-
-TEST_F(SQLConnectionTest, Rollback) {
-  ASSERT_TRUE(db().BeginTransaction());
-  ASSERT_TRUE(db().BeginTransaction());
-  EXPECT_EQ(2, db().GetTransactionNesting());
-  db().RollbackTransaction();
-  EXPECT_FALSE(db().CommitTransaction());
-  EXPECT_TRUE(db().BeginTransaction());
-}
-
-
-
-
-/********************************************************************
- ** Tests from
- ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/statement_unittest.cc
- ********************************************************************/
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    class SQLStatementTest : public SQLConnectionTest
-    {
-    };
-
-    TEST_F(SQLStatementTest, Run) {
-      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)"));
-
-      Statement s(db(), "SELECT b FROM foo WHERE a=?");
-      // Stepping it won't work since we haven't bound the value.
-      EXPECT_FALSE(s.Step());
-
-      // Run should fail since this produces output, and we should use Step(). This
-      // gets a bit wonky since sqlite says this is OK so succeeded is set.
-      s.Reset(true);
-      s.BindInt(0, 3);
-      EXPECT_FALSE(s.Run());
-      EXPECT_EQ(SQLITE_ROW, db().GetErrorCode());
-
-      // Resetting it should put it back to the previous state (not runnable).
-      s.Reset(true);
-
-      // Binding and stepping should produce one row.
-      s.BindInt(0, 3);
-      EXPECT_TRUE(s.Step());
-      EXPECT_EQ(12, s.ColumnInt(0));
-      EXPECT_FALSE(s.Step());
-    }
-
-    TEST_F(SQLStatementTest, BasicErrorCallback) {
-      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a INTEGER PRIMARY KEY, b)"));
-      // Insert in the foo table the primary key. It is an error to insert
-      // something other than an number. This error causes the error callback
-      // handler to be called with SQLITE_MISMATCH as error code.
-      Statement s(db(), "INSERT INTO foo (a) VALUES (?)");
-      s.BindCString(0, "bad bad");
-      EXPECT_THROW(s.Run(), OrthancException);
-    }
-
-    TEST_F(SQLStatementTest, Reset) {
-      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)"));
-      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (4, 13)"));
-
-      Statement s(db(), "SELECT b FROM foo WHERE a = ? ");
-      s.BindInt(0, 3);
-      ASSERT_TRUE(s.Step());
-      EXPECT_EQ(12, s.ColumnInt(0));
-      ASSERT_FALSE(s.Step());
-
-      s.Reset(false);
-      // Verify that we can get all rows again.
-      ASSERT_TRUE(s.Step());
-      EXPECT_EQ(12, s.ColumnInt(0));
-      EXPECT_FALSE(s.Step());
-
-      s.Reset(true);
-      ASSERT_FALSE(s.Step());
-    }
-  }
-}
-
-
-
-
-
-
-/********************************************************************
- ** Tests from
- ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc
- ********************************************************************/
-
-class SQLTransactionTest : public SQLConnectionTest
-{
-public:
-  virtual void SetUp()
-  {
-    SQLConnectionTest::SetUp();
-    ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  }
-
-  // Returns the number of rows in table "foo".
-  int CountFoo() 
-  {
-    Statement count(db(), "SELECT count(*) FROM foo");
-    count.Step();
-    return count.ColumnInt(0);
-  }
-};
-
-
-TEST_F(SQLTransactionTest, Commit) {
-  {
-    Transaction t(db());
-    EXPECT_FALSE(t.IsOpen());
-    t.Begin();
-    EXPECT_TRUE(t.IsOpen());
-
-    EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
-
-    t.Commit();
-    EXPECT_FALSE(t.IsOpen());
-  }
-
-  EXPECT_EQ(1, CountFoo());
-}
-
-TEST_F(SQLTransactionTest, Rollback) {
-  // Test some basic initialization, and that rollback runs when you exit the
-  // scope.
-  {
-    Transaction t(db());
-    EXPECT_FALSE(t.IsOpen());
-    t.Begin();
-    EXPECT_TRUE(t.IsOpen());
-
-    EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
-  }
-
-  // Nothing should have been committed since it was implicitly rolled back.
-  EXPECT_EQ(0, CountFoo());
-
-  // Test explicit rollback.
-  Transaction t2(db());
-  EXPECT_FALSE(t2.IsOpen());
-  t2.Begin();
-
-  EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
-  t2.Rollback();
-  EXPECT_FALSE(t2.IsOpen());
-
-  // Nothing should have been committed since it was explicitly rolled back.
-  EXPECT_EQ(0, CountFoo());
-}
-
-// Rolling back any part of a transaction should roll back all of them.
-TEST_F(SQLTransactionTest, NestedRollback) {
-  EXPECT_EQ(0, db().GetTransactionNesting());
-
-  // Outermost transaction.
-  {
-    Transaction outer(db());
-    outer.Begin();
-    EXPECT_EQ(1, db().GetTransactionNesting());
-
-    // The first inner one gets committed.
-    {
-      Transaction inner1(db());
-      inner1.Begin();
-      EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
-      EXPECT_EQ(2, db().GetTransactionNesting());
-
-      inner1.Commit();
-      EXPECT_EQ(1, db().GetTransactionNesting());
-    }
-
-    // One row should have gotten inserted.
-    EXPECT_EQ(1, CountFoo());
-
-    // The second inner one gets rolled back.
-    {
-      Transaction inner2(db());
-      inner2.Begin();
-      EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
-      EXPECT_EQ(2, db().GetTransactionNesting());
-
-      inner2.Rollback();
-      EXPECT_EQ(1, db().GetTransactionNesting());
-    }
-
-    // A third inner one will fail in Begin since one has already been rolled
-    // back.
-    EXPECT_EQ(1, db().GetTransactionNesting());
-    {
-      Transaction inner3(db());
-      EXPECT_THROW(inner3.Begin(), OrthancException);
-      EXPECT_EQ(1, db().GetTransactionNesting());
-    }
-  }
-  EXPECT_EQ(0, db().GetTransactionNesting());
-  EXPECT_EQ(0, CountFoo());
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/SQLiteChromiumTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,377 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Toolbox.h"
+#include "../Core/SQLite/Connection.h"
+#include "../Core/SQLite/Statement.h"
+#include "../Core/SQLite/Transaction.h"
+
+#include <sqlite3.h>
+
+
+using namespace Orthanc;
+using namespace Orthanc::SQLite;
+
+
+/********************************************************************
+ ** Tests from
+ ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/connection_unittest.cc
+ ********************************************************************/
+
+class SQLConnectionTest : public testing::Test 
+{
+public:
+  SQLConnectionTest()
+  {
+  }
+
+  virtual ~SQLConnectionTest()
+  {
+  }
+
+  virtual void SetUp() 
+  {
+    db_.OpenInMemory();
+  }
+
+  virtual void TearDown() 
+  {
+    db_.Close();
+  }
+
+  Connection& db() 
+  { 
+    return db_; 
+  }
+
+private:
+  Connection db_;
+};
+
+
+
+TEST_F(SQLConnectionTest, Execute) 
+{
+  // Valid statement should return true.
+  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+  EXPECT_EQ(SQLITE_OK, db().GetErrorCode());
+
+  // Invalid statement should fail.
+  ASSERT_EQ(SQLITE_ERROR,
+            db().ExecuteAndReturnErrorCode("CREATE TAB foo (a, b"));
+  EXPECT_EQ(SQLITE_ERROR, db().GetErrorCode());
+}
+
+TEST_F(SQLConnectionTest, ExecuteWithErrorCode) {
+  ASSERT_EQ(SQLITE_OK,
+            db().ExecuteAndReturnErrorCode("CREATE TABLE foo (a, b)"));
+  ASSERT_EQ(SQLITE_ERROR,
+            db().ExecuteAndReturnErrorCode("CREATE TABLE TABLE"));
+  ASSERT_EQ(SQLITE_ERROR,
+            db().ExecuteAndReturnErrorCode(
+              "INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)"));
+}
+
+TEST_F(SQLConnectionTest, CachedStatement) {
+  StatementId id1("foo", 12);
+  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+  ASSERT_TRUE(db().Execute("INSERT INTO foo(a, b) VALUES (12, 13)"));
+
+  // Create a new cached statement.
+  {
+    Statement s(db(), id1, "SELECT a FROM foo");
+    ASSERT_TRUE(s.Step());
+    EXPECT_EQ(12, s.ColumnInt(0));
+  }
+
+  // The statement should be cached still.
+  EXPECT_TRUE(db().HasCachedStatement(id1));
+
+  {
+    // Get the same statement using different SQL. This should ignore our
+    // SQL and use the cached one (so it will be valid).
+    Statement s(db(), id1, "something invalid(");
+    ASSERT_TRUE(s.Step());
+    EXPECT_EQ(12, s.ColumnInt(0));
+  }
+
+  // Make sure other statements aren't marked as cached.
+  EXPECT_FALSE(db().HasCachedStatement(SQLITE_FROM_HERE));
+}
+
+TEST_F(SQLConnectionTest, IsSQLValidTest) {
+  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+  ASSERT_TRUE(db().IsSQLValid("SELECT a FROM foo"));
+  ASSERT_FALSE(db().IsSQLValid("SELECT no_exist FROM foo"));
+}
+
+
+
+TEST_F(SQLConnectionTest, DoesStuffExist) {
+  // Test DoesTableExist.
+  EXPECT_FALSE(db().DoesTableExist("foo"));
+  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+  EXPECT_TRUE(db().DoesTableExist("foo"));
+
+  // Should be case sensitive.
+  EXPECT_FALSE(db().DoesTableExist("FOO"));
+
+  // Test DoesColumnExist.
+  EXPECT_FALSE(db().DoesColumnExist("foo", "bar"));
+  EXPECT_TRUE(db().DoesColumnExist("foo", "a"));
+
+  // Testing for a column on a nonexistent table.
+  EXPECT_FALSE(db().DoesColumnExist("bar", "b"));
+}
+
+TEST_F(SQLConnectionTest, GetLastInsertRowId) {
+  ASSERT_TRUE(db().Execute("CREATE TABLE foo (id INTEGER PRIMARY KEY, value)"));
+
+  ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)"));
+
+  // Last insert row ID should be valid.
+  int64_t row = db().GetLastInsertRowId();
+  EXPECT_LT(0, row);
+
+  // It should be the primary key of the row we just inserted.
+  Statement s(db(), "SELECT value FROM foo WHERE id=?");
+  s.BindInt64(0, row);
+  ASSERT_TRUE(s.Step());
+  EXPECT_EQ(12, s.ColumnInt(0));
+}
+
+TEST_F(SQLConnectionTest, Rollback) {
+  ASSERT_TRUE(db().BeginTransaction());
+  ASSERT_TRUE(db().BeginTransaction());
+  EXPECT_EQ(2, db().GetTransactionNesting());
+  db().RollbackTransaction();
+  EXPECT_FALSE(db().CommitTransaction());
+  EXPECT_TRUE(db().BeginTransaction());
+}
+
+
+
+
+/********************************************************************
+ ** Tests from
+ ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/statement_unittest.cc
+ ********************************************************************/
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class SQLStatementTest : public SQLConnectionTest
+    {
+    };
+
+    TEST_F(SQLStatementTest, Run) {
+      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)"));
+
+      Statement s(db(), "SELECT b FROM foo WHERE a=?");
+      // Stepping it won't work since we haven't bound the value.
+      EXPECT_FALSE(s.Step());
+
+      // Run should fail since this produces output, and we should use Step(). This
+      // gets a bit wonky since sqlite says this is OK so succeeded is set.
+      s.Reset(true);
+      s.BindInt(0, 3);
+      EXPECT_FALSE(s.Run());
+      EXPECT_EQ(SQLITE_ROW, db().GetErrorCode());
+
+      // Resetting it should put it back to the previous state (not runnable).
+      s.Reset(true);
+
+      // Binding and stepping should produce one row.
+      s.BindInt(0, 3);
+      EXPECT_TRUE(s.Step());
+      EXPECT_EQ(12, s.ColumnInt(0));
+      EXPECT_FALSE(s.Step());
+    }
+
+    TEST_F(SQLStatementTest, BasicErrorCallback) {
+      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a INTEGER PRIMARY KEY, b)"));
+      // Insert in the foo table the primary key. It is an error to insert
+      // something other than an number. This error causes the error callback
+      // handler to be called with SQLITE_MISMATCH as error code.
+      Statement s(db(), "INSERT INTO foo (a) VALUES (?)");
+      s.BindCString(0, "bad bad");
+      EXPECT_THROW(s.Run(), OrthancException);
+    }
+
+    TEST_F(SQLStatementTest, Reset) {
+      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)"));
+      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (4, 13)"));
+
+      Statement s(db(), "SELECT b FROM foo WHERE a = ? ");
+      s.BindInt(0, 3);
+      ASSERT_TRUE(s.Step());
+      EXPECT_EQ(12, s.ColumnInt(0));
+      ASSERT_FALSE(s.Step());
+
+      s.Reset(false);
+      // Verify that we can get all rows again.
+      ASSERT_TRUE(s.Step());
+      EXPECT_EQ(12, s.ColumnInt(0));
+      EXPECT_FALSE(s.Step());
+
+      s.Reset(true);
+      ASSERT_FALSE(s.Step());
+    }
+  }
+}
+
+
+
+
+
+
+/********************************************************************
+ ** Tests from
+ ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc
+ ********************************************************************/
+
+class SQLTransactionTest : public SQLConnectionTest
+{
+public:
+  virtual void SetUp()
+  {
+    SQLConnectionTest::SetUp();
+    ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+  }
+
+  // Returns the number of rows in table "foo".
+  int CountFoo() 
+  {
+    Statement count(db(), "SELECT count(*) FROM foo");
+    count.Step();
+    return count.ColumnInt(0);
+  }
+};
+
+
+TEST_F(SQLTransactionTest, Commit) {
+  {
+    Transaction t(db());
+    EXPECT_FALSE(t.IsOpen());
+    t.Begin();
+    EXPECT_TRUE(t.IsOpen());
+
+    EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
+
+    t.Commit();
+    EXPECT_FALSE(t.IsOpen());
+  }
+
+  EXPECT_EQ(1, CountFoo());
+}
+
+TEST_F(SQLTransactionTest, Rollback) {
+  // Test some basic initialization, and that rollback runs when you exit the
+  // scope.
+  {
+    Transaction t(db());
+    EXPECT_FALSE(t.IsOpen());
+    t.Begin();
+    EXPECT_TRUE(t.IsOpen());
+
+    EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
+  }
+
+  // Nothing should have been committed since it was implicitly rolled back.
+  EXPECT_EQ(0, CountFoo());
+
+  // Test explicit rollback.
+  Transaction t2(db());
+  EXPECT_FALSE(t2.IsOpen());
+  t2.Begin();
+
+  EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
+  t2.Rollback();
+  EXPECT_FALSE(t2.IsOpen());
+
+  // Nothing should have been committed since it was explicitly rolled back.
+  EXPECT_EQ(0, CountFoo());
+}
+
+// Rolling back any part of a transaction should roll back all of them.
+TEST_F(SQLTransactionTest, NestedRollback) {
+  EXPECT_EQ(0, db().GetTransactionNesting());
+
+  // Outermost transaction.
+  {
+    Transaction outer(db());
+    outer.Begin();
+    EXPECT_EQ(1, db().GetTransactionNesting());
+
+    // The first inner one gets committed.
+    {
+      Transaction inner1(db());
+      inner1.Begin();
+      EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
+      EXPECT_EQ(2, db().GetTransactionNesting());
+
+      inner1.Commit();
+      EXPECT_EQ(1, db().GetTransactionNesting());
+    }
+
+    // One row should have gotten inserted.
+    EXPECT_EQ(1, CountFoo());
+
+    // The second inner one gets rolled back.
+    {
+      Transaction inner2(db());
+      inner2.Begin();
+      EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
+      EXPECT_EQ(2, db().GetTransactionNesting());
+
+      inner2.Rollback();
+      EXPECT_EQ(1, db().GetTransactionNesting());
+    }
+
+    // A third inner one will fail in Begin since one has already been rolled
+    // back.
+    EXPECT_EQ(1, db().GetTransactionNesting());
+    {
+      Transaction inner3(db());
+      EXPECT_THROW(inner3.Begin(), OrthancException);
+      EXPECT_EQ(1, db().GetTransactionNesting());
+    }
+  }
+  EXPECT_EQ(0, db().GetTransactionNesting());
+  EXPECT_EQ(0, CountFoo());
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/SQLiteTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,334 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Toolbox.h"
+#include "../Core/SQLite/Connection.h"
+#include "../Core/SQLite/Statement.h"
+#include "../Core/SQLite/Transaction.h"
+
+#include <sqlite3.h>
+
+using namespace Orthanc;
+
+
+TEST(SQLite, Configuration)
+{
+  ASSERT_EQ(1, sqlite3_threadsafe());
+}
+
+
+TEST(SQLite, Connection)
+{
+  Toolbox::RemoveFile("UnitTestsResults/coucou");
+  SQLite::Connection c;
+  c.Open("UnitTestsResults/coucou");
+  c.Execute("CREATE TABLE c(k INTEGER PRIMARY KEY AUTOINCREMENT, v INTEGER)");
+  c.Execute("INSERT INTO c VALUES(NULL, 42);");
+}
+
+
+TEST(SQLite, StatementReferenceBasic)
+{
+  sqlite3* db;
+  sqlite3_open(":memory:", &db);
+
+  {
+    SQLite::StatementReference r(db, "SELECT * FROM sqlite_master");
+    ASSERT_EQ(0u, r.GetReferenceCount());
+
+    {
+      SQLite::StatementReference r1(r);
+      ASSERT_EQ(1u, r.GetReferenceCount());
+      ASSERT_EQ(0u, r1.GetReferenceCount());
+
+      {
+        SQLite::StatementReference r2(r);
+        ASSERT_EQ(2u, r.GetReferenceCount());
+        ASSERT_EQ(0u, r1.GetReferenceCount());
+        ASSERT_EQ(0u, r2.GetReferenceCount());
+
+        SQLite::StatementReference r3(r2);
+        ASSERT_EQ(3u, r.GetReferenceCount());
+        ASSERT_EQ(0u, r1.GetReferenceCount());
+        ASSERT_EQ(0u, r2.GetReferenceCount());
+        ASSERT_EQ(0u, r3.GetReferenceCount());
+      }
+
+      ASSERT_EQ(1u, r.GetReferenceCount());
+      ASSERT_EQ(0u, r1.GetReferenceCount());
+
+      {
+        SQLite::StatementReference r2(r);
+        ASSERT_EQ(2u, r.GetReferenceCount());
+        ASSERT_EQ(0u, r1.GetReferenceCount());
+        ASSERT_EQ(0u, r2.GetReferenceCount());
+      }
+
+      ASSERT_EQ(1u, r.GetReferenceCount());
+      ASSERT_EQ(0u, r1.GetReferenceCount());
+    }
+
+    ASSERT_EQ(0u, r.GetReferenceCount());
+  }
+
+  sqlite3_close(db);
+}
+
+TEST(SQLite, StatementBasic)
+{
+  SQLite::Connection c;
+  c.OpenInMemory();
+  
+  SQLite::Statement s(c, "SELECT * from sqlite_master");
+  s.Run();
+
+  for (unsigned int i = 0; i < 5; i++)
+  {
+    SQLite::Statement cs(c, SQLITE_FROM_HERE, "SELECT * from sqlite_master");
+    cs.Step();
+  }
+}
+
+
+namespace
+{
+  static bool destroyed;
+
+  class MyFunc : public SQLite::IScalarFunction
+  {
+  public:
+    MyFunc()
+    {
+      destroyed = false;
+    }
+
+    virtual ~MyFunc()
+    {
+      destroyed = true;
+    }
+
+    virtual const char* GetName() const
+    {
+      return "MYFUNC";
+    }
+
+    virtual unsigned int GetCardinality() const
+    {
+      return 2;
+    }
+
+    virtual void Compute(SQLite::FunctionContext& context)
+    {
+      context.SetIntResult(1000 + context.GetIntValue(0) * context.GetIntValue(1));
+    }
+  };
+
+  class MyDelete : public SQLite::IScalarFunction
+  {
+  public:
+    std::set<int> deleted_;
+
+    virtual const char* GetName() const
+    {
+      return "MYDELETE";
+    }
+
+    virtual unsigned int GetCardinality() const
+    {
+      return 1;
+    }
+
+    virtual void Compute(SQLite::FunctionContext& context)
+    {
+      deleted_.insert(context.GetIntValue(0));
+      context.SetNullResult();
+    }
+  };
+}
+
+TEST(SQLite, ScalarFunction)
+{
+  {
+    SQLite::Connection c;
+    c.OpenInMemory();
+    c.Register(new MyFunc());
+    c.Execute("CREATE TABLE t(id INTEGER PRIMARY KEY, v1 INTEGER, v2 INTEGER);");
+    c.Execute("INSERT INTO t VALUES(NULL, 2, 3);");
+    c.Execute("INSERT INTO t VALUES(NULL, 4, 4);");
+    c.Execute("INSERT INTO t VALUES(NULL, 6, 5);");
+    SQLite::Statement t(c, "SELECT MYFUNC(v1, v2), v1, v2 FROM t");
+    int i = 0;
+    while (t.Step())
+    {
+      ASSERT_EQ(t.ColumnInt(0), 1000 + t.ColumnInt(1) * t.ColumnInt(2));
+      i++;
+    }
+    ASSERT_EQ(3, i);
+    ASSERT_FALSE(destroyed);
+  }
+  ASSERT_TRUE(destroyed);
+}
+
+TEST(SQLite, CascadedDeleteCallback)
+{
+  SQLite::Connection c;
+  c.OpenInMemory();
+  MyDelete *func = new MyDelete();
+  c.Register(func);
+  c.Execute("CREATE TABLE parent(id INTEGER PRIMARY KEY, dummy INTEGER);");
+  c.Execute("CREATE TABLE child("
+            "  id INTEGER PRIMARY KEY, "
+            "  parent INTEGER REFERENCES parent(id) ON DELETE CASCADE, "
+            "  value INTEGER);");
+  c.Execute("CREATE TRIGGER childRemoved "
+            "AFTER DELETE ON child "
+            "FOR EACH ROW BEGIN "
+            "  SELECT MYDELETE(old.value); "
+            "END;");
+
+  c.Execute("INSERT INTO parent VALUES(42, 100);");
+  c.Execute("INSERT INTO parent VALUES(43, 101);");
+
+  c.Execute("INSERT INTO child VALUES(NULL, 42, 4200);");
+  c.Execute("INSERT INTO child VALUES(NULL, 42, 4201);");
+
+  c.Execute("INSERT INTO child VALUES(NULL, 43, 4300);");
+  c.Execute("INSERT INTO child VALUES(NULL, 43, 4301);");
+
+  // The following command deletes "parent(43, 101)", then in turns
+  // "child(NULL, 43, 4300/4301)", then calls the MyDelete on 4300 and
+  // 4301
+  c.Execute("DELETE FROM parent WHERE dummy=101");
+
+  ASSERT_EQ(2u, func->deleted_.size());
+  ASSERT_TRUE(func->deleted_.find(4300) != func->deleted_.end());
+  ASSERT_TRUE(func->deleted_.find(4301) != func->deleted_.end());
+}
+
+
+TEST(SQLite, EmptyTransactions)
+{
+  try
+  {
+    SQLite::Connection c;
+    c.OpenInMemory();
+
+    c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY);");
+    c.Execute("INSERT INTO a VALUES(NULL)");
+      
+    {
+      SQLite::Transaction t(c);
+      t.Begin();
+      {
+        SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a");
+        s.Step();
+      }
+      //t.Commit();
+    }
+
+    {
+      SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a");
+      s.Step();
+    }
+  }
+  catch (OrthancException& e)
+  {
+    fprintf(stderr, "Exception: [%s]\n", e.What());
+    throw e;
+  }
+}
+
+
+TEST(SQLite, Types)
+{
+  SQLite::Connection c;
+  c.OpenInMemory();
+  c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY, value)");
+
+  {
+    SQLite::Statement s(c, std::string("SELECT * FROM a"));
+    ASSERT_EQ(2, s.ColumnCount());
+    ASSERT_FALSE(s.Step());
+  }
+
+  {
+    SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a"));
+    ASSERT_FALSE(s.Step());
+    ASSERT_EQ("SELECT * FROM a", s.GetOriginalSQLStatement());
+  }
+
+  {
+    SQLite::Statement s(c, SQLITE_FROM_HERE, "INSERT INTO a VALUES(NULL, ?);");
+    s.BindNull(0);             ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindBool(0, true);       ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindInt(0, 42);          ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindInt64(0, 42ll);      ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindDouble(0, 42.5);     ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindCString(0, "Hello"); ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindBlob(0, "Hello", 5); ASSERT_TRUE(s.Run()); s.Reset();
+  }
+
+  {
+    SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a"));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_NULL, s.GetColumnType(1));
+    ASSERT_TRUE(s.ColumnIsNull(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
+    ASSERT_TRUE(s.ColumnBool(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
+    ASSERT_EQ(42, s.ColumnInt(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
+    ASSERT_EQ(42ll, s.ColumnInt64(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_FLOAT, s.GetColumnType(1));
+    ASSERT_DOUBLE_EQ(42.5, s.ColumnDouble(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_TEXT, s.GetColumnType(1));
+    ASSERT_EQ("Hello", s.ColumnString(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_BLOB, s.GetColumnType(1));
+    ASSERT_EQ(5, s.ColumnByteLength(1));
+    ASSERT_TRUE(!memcmp("Hello", s.ColumnBlob(1), 5));
+
+    std::string t;
+    ASSERT_TRUE(s.ColumnBlobAsString(1, &t));
+    ASSERT_EQ("Hello", t);
+
+    ASSERT_FALSE(s.Step());
+  }
+}
--- a/UnitTestsSources/ServerIndexTests.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/UnitTestsSources/ServerIndexTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -38,6 +38,7 @@
 #include "../OrthancServer/ServerIndex.h"
 #include "../Core/Uuid.h"
 #include "../Core/DicomFormat/DicomNullValue.h"
+#include "../Core/FileStorage/FilesystemStorage.h"
 
 #include <ctype.h>
 #include <glog/logging.h>
@@ -57,6 +58,7 @@
   {
   public:
     std::vector<std::string> deletedFiles_;
+    std::vector<std::string> deletedResources_;
     std::string ancestorId_;
     ResourceType ancestorType_;
 
@@ -78,7 +80,20 @@
       const std::string fileUuid = info.GetUuid();
       deletedFiles_.push_back(fileUuid);
       LOG(INFO) << "A file must be removed: " << fileUuid;
-    }                                
+    }       
+
+    virtual void SignalChange(const ServerIndexChange& change)
+    {
+      if (change.GetChangeType() == ChangeType_Deleted)
+      {
+        deletedResources_.push_back(change.GetPublicId());        
+      }
+
+      LOG(INFO) << "Change related to resource " << change.GetPublicId() << " of type " 
+                << EnumerationToString(change.GetResourceType()) << ": " 
+                << EnumerationToString(change.GetChangeType());
+    }
+
   };
 
 
@@ -86,7 +101,7 @@
   {
   protected:
     std::auto_ptr<ServerIndexListener> listener_;
-    std::auto_ptr<DatabaseWrapper> index_;
+    std::auto_ptr<IDatabaseWrapper> index_;
 
     DatabaseWrapperTest()
     {
@@ -95,7 +110,18 @@
     virtual void SetUp() 
     {
       listener_.reset(new ServerIndexListener);
-      index_.reset(new DatabaseWrapper(*listener_));
+
+      switch (GetParam())
+      {
+        case DatabaseWrapperClass_SQLite:
+          index_.reset(new DatabaseWrapper());
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      index_->SetListener(*listener_);
     }
 
     virtual void TearDown()
@@ -103,6 +129,121 @@
       index_.reset(NULL);
       listener_.reset(NULL);
     }
+
+    void CheckTableRecordCount(uint32_t expected, const char* table)
+    {
+      switch (GetParam())
+      {
+        case DatabaseWrapperClass_SQLite:
+        {
+          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          ASSERT_EQ(expected, sqlite->GetTableRecordCount(table));
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    void CheckNoParent(int64_t id)
+    {
+      std::string s;
+
+      switch (GetParam())
+      {
+        case DatabaseWrapperClass_SQLite:
+        {
+          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          ASSERT_FALSE(sqlite->GetParentPublicId(s, id));
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    void CheckParentPublicId(const char* expected, int64_t id)
+    {
+      std::string s;
+
+      switch (GetParam())
+      {
+        case DatabaseWrapperClass_SQLite:
+        {
+          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          ASSERT_TRUE(sqlite->GetParentPublicId(s, id));
+          ASSERT_EQ(expected, s);
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    void CheckNoChild(int64_t id)
+    {
+      std::list<std::string> j;
+
+      switch (GetParam())
+      {
+        case DatabaseWrapperClass_SQLite:
+        {
+          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          sqlite->GetChildren(j, id);
+          ASSERT_EQ(0, j.size());
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    void CheckOneChild(const char* expected, int64_t id)
+    {
+      std::list<std::string> j;
+
+      switch (GetParam())
+      {
+        case DatabaseWrapperClass_SQLite:
+        {
+          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          sqlite->GetChildren(j, id);
+          ASSERT_EQ(1, j.size());
+          ASSERT_EQ(expected, j.front());
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    void CheckTwoChildren(const char* expected1,
+                          const char* expected2,
+                          int64_t id)
+    {
+      std::list<std::string> j;
+
+      switch (GetParam())
+      {
+        case DatabaseWrapperClass_SQLite:
+        {
+          DatabaseWrapper* sqlite = dynamic_cast<DatabaseWrapper*>(index_.get());
+          sqlite->GetChildren(j, id);
+          ASSERT_EQ(2, j.size());
+          ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) ||
+                      (expected1 == j.back() && expected2 == j.front()));                    
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
   };
 }
 
@@ -141,15 +282,15 @@
   ASSERT_EQ(ResourceType_Study, index_->GetResourceType(a[6]));
 
   {
-    Json::Value t;
+    std::list<std::string> t;
     index_->GetAllPublicIds(t, ResourceType_Patient);
 
     ASSERT_EQ(1u, t.size());
-    ASSERT_EQ("a", t[0u].asString());
+    ASSERT_EQ("a", t.front());
 
     index_->GetAllPublicIds(t, ResourceType_Series);
     ASSERT_EQ(1u, t.size());
-    ASSERT_EQ("c", t[0u].asString());
+    ASSERT_EQ("c", t.front());
 
     index_->GetAllPublicIds(t, ResourceType_Study);
     ASSERT_EQ(2u, t.size());
@@ -176,14 +317,14 @@
   ASSERT_FALSE(index_->LookupParent(parent, a[6]));
 
   std::string s;
-  
-  ASSERT_FALSE(index_->GetParentPublicId(s, a[0]));
-  ASSERT_FALSE(index_->GetParentPublicId(s, a[6]));
-  ASSERT_TRUE(index_->GetParentPublicId(s, a[1])); ASSERT_EQ("a", s);
-  ASSERT_TRUE(index_->GetParentPublicId(s, a[2])); ASSERT_EQ("b", s);
-  ASSERT_TRUE(index_->GetParentPublicId(s, a[3])); ASSERT_EQ("c", s);
-  ASSERT_TRUE(index_->GetParentPublicId(s, a[4])); ASSERT_EQ("c", s);
-  ASSERT_TRUE(index_->GetParentPublicId(s, a[5])); ASSERT_EQ("g", s);
+
+  CheckNoParent(a[0]);
+  CheckNoParent(a[6]);
+  CheckParentPublicId("a", a[1]);
+  CheckParentPublicId("b", a[2]);
+  CheckParentPublicId("c", a[3]);
+  CheckParentPublicId("c", a[4]);
+  CheckParentPublicId("g", a[5]);
 
   std::list<std::string> l;
   index_->GetChildrenPublicId(l, a[0]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("b", l.front());
@@ -209,7 +350,7 @@
   ASSERT_EQ(0u, md.size());
 
   index_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", 
-                                     CompressionType_Zlib, 21, "compressedMD5"));
+                                       CompressionType_Zlib, 21, "compressedMD5"));
   index_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"));
   index_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"));
   index_->SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE");
@@ -220,35 +361,46 @@
   index_->SetMetadata(a[4], MetadataType_ModifiedFrom, "TUTU");
   index_->ListAvailableMetadata(md, a[4]);
   ASSERT_EQ(2u, md.size());
+
+  std::map<MetadataType, std::string> md2;
+  index_->GetAllMetadata(md2, a[4]);
+  ASSERT_EQ(2u, md2.size());
+  ASSERT_EQ("TUTU", md2[MetadataType_ModifiedFrom]);
+  ASSERT_EQ("PINNACLE", md2[MetadataType_Instance_RemoteAet]);
+
   index_->DeleteMetadata(a[4], MetadataType_ModifiedFrom);
   index_->ListAvailableMetadata(md, a[4]);
   ASSERT_EQ(1u, md.size());
   ASSERT_EQ(MetadataType_Instance_RemoteAet, md.front());
 
+  index_->GetAllMetadata(md2, a[4]);
+  ASSERT_EQ(1u, md2.size());
+  ASSERT_EQ("PINNACLE", md2[MetadataType_Instance_RemoteAet]);
+
+
   ASSERT_EQ(21u + 42u + 44u, index_->GetTotalCompressedSize());
   ASSERT_EQ(42u + 42u + 44u, index_->GetTotalUncompressedSize());
 
-  DicomMap m;
-  m.SetValue(0x0010, 0x0010, "PatientName");
-  index_->SetMainDicomTags(a[3], m);
+  index_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName");
 
   int64_t b;
   ResourceType t;
-  ASSERT_TRUE(index_->LookupResource("g", b, t));
+  ASSERT_TRUE(index_->LookupResource(b, t, "g"));
   ASSERT_EQ(7, b);
   ASSERT_EQ(ResourceType_Study, t);
 
   ASSERT_TRUE(index_->LookupMetadata(s, a[4], MetadataType_Instance_RemoteAet));
   ASSERT_FALSE(index_->LookupMetadata(s, a[4], MetadataType_Instance_IndexInSeries));
   ASSERT_EQ("PINNACLE", s);
-  ASSERT_EQ("PINNACLE", index_->GetMetadata(a[4], MetadataType_Instance_RemoteAet));
-  ASSERT_EQ("None", index_->GetMetadata(a[4], MetadataType_Instance_IndexInSeries, "None"));
+
+  std::string u;
+  ASSERT_TRUE(index_->LookupMetadata(u, a[4], MetadataType_Instance_RemoteAet));
+  ASSERT_EQ("PINNACLE", u);
+  ASSERT_FALSE(index_->LookupMetadata(u, a[4], MetadataType_Instance_IndexInSeries));
 
   ASSERT_TRUE(index_->LookupGlobalProperty(s, GlobalProperty_FlushSleep));
   ASSERT_FALSE(index_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42)));
   ASSERT_EQ("World", s);
-  ASSERT_EQ("World", index_->GetGlobalProperty(GlobalProperty_FlushSleep));
-  ASSERT_EQ("None", index_->GetGlobalProperty(static_cast<GlobalProperty>(42), "None"));
 
   FileInfo att;
   ASSERT_TRUE(index_->LookupAttachment(att, a[4], FileContentType_DicomAsJson));
@@ -268,12 +420,15 @@
   ASSERT_EQ(CompressionType_None, att.GetCompressionType());
 
   ASSERT_EQ(0u, listener_->deletedFiles_.size());
-  ASSERT_EQ(7u, index_->GetTableRecordCount("Resources")); 
-  ASSERT_EQ(3u, index_->GetTableRecordCount("AttachedFiles"));
-  ASSERT_EQ(1u, index_->GetTableRecordCount("Metadata"));
-  ASSERT_EQ(1u, index_->GetTableRecordCount("MainDicomTags"));
+  ASSERT_EQ(0u, listener_->deletedResources_.size());
+
+  CheckTableRecordCount(7, "Resources");
+  CheckTableRecordCount(3, "AttachedFiles");
+  CheckTableRecordCount(1, "Metadata");
+  CheckTableRecordCount(1, "MainDicomTags");
+
   index_->DeleteResource(a[0]);
-
+  ASSERT_EQ(5u, listener_->deletedResources_.size());
   ASSERT_EQ(2u, listener_->deletedFiles_.size());
   ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
                          listener_->deletedFiles_.end(),
@@ -282,14 +437,17 @@
                          listener_->deletedFiles_.end(),
                          "my dicom file") == listener_->deletedFiles_.end());
 
-  ASSERT_EQ(2u, index_->GetTableRecordCount("Resources"));
-  ASSERT_EQ(0u, index_->GetTableRecordCount("Metadata"));
-  ASSERT_EQ(1u, index_->GetTableRecordCount("AttachedFiles"));
-  ASSERT_EQ(0u, index_->GetTableRecordCount("MainDicomTags"));
+  CheckTableRecordCount(2, "Resources");
+  CheckTableRecordCount(0, "Metadata");
+  CheckTableRecordCount(1, "AttachedFiles");
+  CheckTableRecordCount(0, "MainDicomTags");
+
   index_->DeleteResource(a[5]);
-  ASSERT_EQ(0u, index_->GetTableRecordCount("Resources"));
-  ASSERT_EQ(0u, index_->GetTableRecordCount("AttachedFiles"));
-  ASSERT_EQ(2u, index_->GetTableRecordCount("GlobalProperties"));
+  ASSERT_EQ(7u, listener_->deletedResources_.size());
+
+  CheckTableRecordCount(0, "Resources");
+  CheckTableRecordCount(0, "AttachedFiles");
+  CheckTableRecordCount(2, "GlobalProperties");
 
   ASSERT_EQ(3u, listener_->deletedFiles_.size());
   ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
@@ -321,29 +479,14 @@
   index_->AttachChild(a[0], a[5]);
   index_->AttachChild(a[5], a[7]);
 
-  {
-    Json::Value j;
-    index_->GetChildren(j, a[0]);
-    ASSERT_EQ(2u, j.size());
-    ASSERT_TRUE((j[0u] == "b" && j[1u] == "f") ||
-                (j[1u] == "b" && j[0u] == "f"));
-
-    index_->GetChildren(j, a[1]);
-    ASSERT_EQ(2u, j.size());
-    ASSERT_TRUE((j[0u] == "c" && j[1u] == "g") ||
-                (j[1u] == "c" && j[0u] == "g"));
-
-    index_->GetChildren(j, a[2]);
-    ASSERT_EQ(2u, j.size());
-    ASSERT_TRUE((j[0u] == "d" && j[1u] == "e") ||
-                (j[1u] == "d" && j[0u] == "e"));
-
-    index_->GetChildren(j, a[3]); ASSERT_EQ(0u, j.size());
-    index_->GetChildren(j, a[4]); ASSERT_EQ(0u, j.size());
-    index_->GetChildren(j, a[5]); ASSERT_EQ(1u, j.size()); ASSERT_EQ("h", j[0u].asString());
-    index_->GetChildren(j, a[6]); ASSERT_EQ(0u, j.size());
-    index_->GetChildren(j, a[7]); ASSERT_EQ(0u, j.size());
-  }
+  CheckTwoChildren("b", "f", a[0]);
+  CheckTwoChildren("c", "g", a[1]);
+  CheckTwoChildren("d", "e", a[2]);
+  CheckNoChild(a[3]);
+  CheckNoChild(a[4]);
+  CheckOneChild("h", a[5]);
+  CheckNoChild(a[6]);
+  CheckNoChild(a[7]);
 
   listener_->Reset();
   index_->DeleteResource(a[3]);
@@ -374,19 +517,22 @@
     std::string p = "Patient " + boost::lexical_cast<std::string>(i);
     patients.push_back(index_->CreateResource(p, ResourceType_Patient));
     index_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, 
-                                              "md5-" + boost::lexical_cast<std::string>(i)));
+                                                "md5-" + boost::lexical_cast<std::string>(i)));
     ASSERT_FALSE(index_->IsProtectedPatient(patients[i]));
   }
 
-  ASSERT_EQ(10u, index_->GetTableRecordCount("Resources")); 
-  ASSERT_EQ(10u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+  CheckTableRecordCount(10u, "Resources");
+  CheckTableRecordCount(10u, "PatientRecyclingOrder");
 
   listener_->Reset();
+  ASSERT_EQ(0u, listener_->deletedResources_.size());
 
   index_->DeleteResource(patients[5]);
   index_->DeleteResource(patients[0]);
-  ASSERT_EQ(8u, index_->GetTableRecordCount("Resources")); 
-  ASSERT_EQ(8u, index_->GetTableRecordCount("PatientRecyclingOrder"));
+  ASSERT_EQ(2u, listener_->deletedResources_.size());
+
+  CheckTableRecordCount(8u, "Resources");
+  CheckTableRecordCount(8u, "PatientRecyclingOrder");
 
   ASSERT_EQ(2u, listener_->deletedFiles_.size());
   ASSERT_EQ("Patient 5", listener_->deletedFiles_[0]);
@@ -395,24 +541,32 @@
   int64_t p;
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
   index_->DeleteResource(p);
+  ASSERT_EQ(3u, listener_->deletedResources_.size());
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
   index_->DeleteResource(p);
+  ASSERT_EQ(4u, listener_->deletedResources_.size());
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
   index_->DeleteResource(p);
+  ASSERT_EQ(5u, listener_->deletedResources_.size());
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
   index_->DeleteResource(p);
+  ASSERT_EQ(6u, listener_->deletedResources_.size());
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]);
   index_->DeleteResource(p);
   index_->DeleteResource(patients[8]);
+  ASSERT_EQ(8u, listener_->deletedResources_.size());
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]);
   index_->DeleteResource(p);
+  ASSERT_EQ(9u, listener_->deletedResources_.size());
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]);
   index_->DeleteResource(p);
   ASSERT_FALSE(index_->SelectPatientToRecycle(p));
+  ASSERT_EQ(10u, listener_->deletedResources_.size());
 
   ASSERT_EQ(10u, listener_->deletedFiles_.size());
-  ASSERT_EQ(0u, index_->GetTableRecordCount("Resources")); 
-  ASSERT_EQ(0u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+
+  CheckTableRecordCount(0, "Resources");
+  CheckTableRecordCount(0, "PatientRecyclingOrder");
 }
 
 
@@ -424,88 +578,101 @@
     std::string p = "Patient " + boost::lexical_cast<std::string>(i);
     patients.push_back(index_->CreateResource(p, ResourceType_Patient));
     index_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10,
-                                              "md5-" + boost::lexical_cast<std::string>(i)));
+                                                "md5-" + boost::lexical_cast<std::string>(i)));
     ASSERT_FALSE(index_->IsProtectedPatient(patients[i]));
   }
 
-  ASSERT_EQ(5u, index_->GetTableRecordCount("Resources")); 
-  ASSERT_EQ(5u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+  CheckTableRecordCount(5, "Resources");
+  CheckTableRecordCount(5, "PatientRecyclingOrder");
 
   ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
   index_->SetProtectedPatient(patients[2], true);
   ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
-  ASSERT_EQ(4u, index_->GetTableRecordCount("PatientRecyclingOrder"));
-  ASSERT_EQ(5u, index_->GetTableRecordCount("Resources")); 
+  CheckTableRecordCount(5, "Resources");
+  CheckTableRecordCount(4, "PatientRecyclingOrder");
 
   index_->SetProtectedPatient(patients[2], true);
   ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
-  ASSERT_EQ(4u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+  CheckTableRecordCount(4, "PatientRecyclingOrder");
   index_->SetProtectedPatient(patients[2], false);
   ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
-  ASSERT_EQ(5u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+  CheckTableRecordCount(5, "PatientRecyclingOrder");
   index_->SetProtectedPatient(patients[2], false);
   ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
-  ASSERT_EQ(5u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
-
-  ASSERT_EQ(5u, index_->GetTableRecordCount("Resources")); 
-  ASSERT_EQ(5u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+  CheckTableRecordCount(5, "PatientRecyclingOrder");
+  CheckTableRecordCount(5, "Resources");
   index_->SetProtectedPatient(patients[2], true);
   ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
-  ASSERT_EQ(4u, index_->GetTableRecordCount("PatientRecyclingOrder"));
+  CheckTableRecordCount(4, "PatientRecyclingOrder");
   index_->SetProtectedPatient(patients[2], false);
   ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
-  ASSERT_EQ(5u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+  CheckTableRecordCount(5, "PatientRecyclingOrder");
   index_->SetProtectedPatient(patients[3], true);
   ASSERT_TRUE(index_->IsProtectedPatient(patients[3]));
-  ASSERT_EQ(4u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+  CheckTableRecordCount(4, "PatientRecyclingOrder");
 
-  ASSERT_EQ(5u, index_->GetTableRecordCount("Resources")); 
+  CheckTableRecordCount(5, "Resources");
   ASSERT_EQ(0u, listener_->deletedFiles_.size());
 
   // Unprotecting a patient puts it at the last position in the recycling queue
   int64_t p;
+  ASSERT_EQ(0u, listener_->deletedResources_.size());
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]);
   index_->DeleteResource(p);
+  ASSERT_EQ(1u, listener_->deletedResources_.size());
   ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]);
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
   index_->DeleteResource(p);
+  ASSERT_EQ(2u, listener_->deletedResources_.size());
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
   index_->DeleteResource(p);
+  ASSERT_EQ(3u, listener_->deletedResources_.size());
   ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[2]));
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
   index_->DeleteResource(p);
+  ASSERT_EQ(4u, listener_->deletedResources_.size());
   // "patients[3]" is still protected
   ASSERT_FALSE(index_->SelectPatientToRecycle(p));
 
   ASSERT_EQ(4u, listener_->deletedFiles_.size());
-  ASSERT_EQ(1u, index_->GetTableRecordCount("Resources")); 
-  ASSERT_EQ(0u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+  CheckTableRecordCount(1, "Resources");
+  CheckTableRecordCount(0, "PatientRecyclingOrder");
 
   index_->SetProtectedPatient(patients[3], false);
-  ASSERT_EQ(1u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+  CheckTableRecordCount(1, "PatientRecyclingOrder");
   ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[3]));
   ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[2]));
   ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
   index_->DeleteResource(p);
+  ASSERT_EQ(5u, listener_->deletedResources_.size());
 
   ASSERT_EQ(5u, listener_->deletedFiles_.size());
-  ASSERT_EQ(0u, index_->GetTableRecordCount("Resources")); 
-  ASSERT_EQ(0u, index_->GetTableRecordCount("PatientRecyclingOrder")); 
+  CheckTableRecordCount(0, "Resources");
+  CheckTableRecordCount(0, "PatientRecyclingOrder");
 }
 
 
 
-TEST_P(DatabaseWrapperTest, Sequence)
+TEST(ServerIndex, Sequence)
 {
-  ASSERT_EQ(1u, index_->IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
-  ASSERT_EQ(2u, index_->IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
-  ASSERT_EQ(3u, index_->IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
-  ASSERT_EQ(4u, index_->IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
+  const std::string path = "UnitTestsStorage";
+
+  Toolbox::RemoveFile(path + "/index");
+  FilesystemStorage storage(path);
+  DatabaseWrapper db;   // The SQLite DB is in memory
+  ServerContext context(db);
+  context.SetStorageArea(storage);
+  ServerIndex& index = context.GetIndex();
+
+  ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
+  ASSERT_EQ(2u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
+  ASSERT_EQ(3u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
+  ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
 }
 
 
 
-TEST_P(DatabaseWrapperTest, LookupTagValue)
+TEST_P(DatabaseWrapperTest, LookupIdentifier)
 {
   int64_t a[] = {
     index_->CreateResource("a", ResourceType_Study),   // 0
@@ -514,42 +681,41 @@
     index_->CreateResource("d", ResourceType_Series)   // 3
   };
 
-  DicomMap m;
-  m.Clear(); m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "0"); index_->SetMainDicomTags(a[0], m);
-  m.Clear(); m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "1"); index_->SetMainDicomTags(a[1], m);
-  m.Clear(); m.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "0"); index_->SetMainDicomTags(a[2], m);
-  m.Clear(); m.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "0"); index_->SetMainDicomTags(a[3], m);
+  index_->SetMainDicomTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0");
+  index_->SetMainDicomTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1");
+  index_->SetMainDicomTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0");
+  index_->SetMainDicomTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0");
 
   std::list<int64_t> s;
 
-  index_->LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "0");
+  index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "0");
   ASSERT_EQ(2u, s.size());
   ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end());
   ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end());
 
-  index_->LookupTagValue(s, "0");
+  index_->LookupIdentifier(s, "0");
   ASSERT_EQ(3u, s.size());
   ASSERT_TRUE(std::find(s.begin(), s.end(), a[0]) != s.end());
   ASSERT_TRUE(std::find(s.begin(), s.end(), a[2]) != s.end());
   ASSERT_TRUE(std::find(s.begin(), s.end(), a[3]) != s.end());
 
-  index_->LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "1");
+  index_->LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1");
   ASSERT_EQ(1u, s.size());
   ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end());
 
-  index_->LookupTagValue(s, "1");
+  index_->LookupIdentifier(s, "1");
   ASSERT_EQ(1u, s.size());
   ASSERT_TRUE(std::find(s.begin(), s.end(), a[1]) != s.end());
 
 
   /*{
-      std::list<std::string> s;
-      context.GetIndex().LookupTagValue(s, DICOM_TAG_STUDY_INSTANCE_UID, "1.2.250.1.74.20130819132500.29000036381059");
-      for (std::list<std::string>::iterator i = s.begin(); i != s.end(); i++)
-      {
-        std::cout << "*** " << *i << std::endl;;
-      }      
-      }*/
+    std::list<std::string> s;
+    context.GetIndex().LookupIdentifier(s, DICOM_TAG_STUDY_INSTANCE_UID, "1.2.250.1.74.20130819132500.29000036381059");
+    for (std::list<std::string>::iterator i = s.begin(); i != s.end(); i++)
+    {
+    std::cout << "*** " << *i << std::endl;;
+    }      
+    }*/
 }
 
 
@@ -557,8 +723,12 @@
 TEST(ServerIndex, AttachmentRecycling)
 {
   const std::string path = "UnitTestsStorage";
+
   Toolbox::RemoveFile(path + "/index");
-  ServerContext context(path, ":memory:");   // The SQLite DB is in memory
+  FilesystemStorage storage(path);
+  DatabaseWrapper db;   // The SQLite DB is in memory
+  ServerContext context(db);
+  context.SetStorageArea(storage);
   ServerIndex& index = context.GetIndex();
 
   index.SetMaximumStorageSize(10);
@@ -579,7 +749,13 @@
     instance.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "study-" + id);
     instance.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, "series-" + id);
     instance.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "instance-" + id);
-    ASSERT_EQ(StoreStatus_Success, index.Store(instance, attachments, ""));
+
+    std::map<MetadataType, std::string> instanceMetadata;
+    ServerIndex::MetadataMap metadata;
+    ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, instance, attachments, "", metadata));
+    ASSERT_EQ(2, instanceMetadata.size());
+    ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_RemoteAet) != instanceMetadata.end());
+    ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_ReceptionDate) != instanceMetadata.end());
 
     DicomInstanceHasher hasher(instance);
     ids.push_back(hasher.HashPatient());
--- a/UnitTestsSources/UnitTestsMain.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Thu May 21 16:58:30 2015 +0200
@@ -1,7 +1,7 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * Belgium
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
  *
  * This program is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License as
@@ -45,6 +45,7 @@
 #include "../Core/Uuid.h"
 #include "../OrthancServer/OrthancInitialization.h"
 
+
 using namespace Orthanc;
 
 
@@ -174,45 +175,96 @@
 }
 
 
-TEST(ParseGetQuery, Basic)
+TEST(ParseGetArguments, Basic)
 {
+  HttpHandler::GetArguments b;
+  HttpHandler::ParseGetArguments(b, "aaa=baaa&bb=a&aa=c");
+
   HttpHandler::Arguments a;
-  HttpHandler::ParseGetQuery(a, "aaa=baaa&bb=a&aa=c");
+  HttpHandler::CompileGetArguments(a, b);
+
   ASSERT_EQ(3u, a.size());
   ASSERT_EQ(a["aaa"], "baaa");
   ASSERT_EQ(a["bb"], "a");
   ASSERT_EQ(a["aa"], "c");
 }
 
-TEST(ParseGetQuery, BasicEmpty)
+TEST(ParseGetArguments, BasicEmpty)
 {
+  HttpHandler::GetArguments b;
+  HttpHandler::ParseGetArguments(b, "aaa&bb=aa&aa");
+
   HttpHandler::Arguments a;
-  HttpHandler::ParseGetQuery(a, "aaa&bb=aa&aa");
+  HttpHandler::CompileGetArguments(a, b);
+
   ASSERT_EQ(3u, a.size());
   ASSERT_EQ(a["aaa"], "");
   ASSERT_EQ(a["bb"], "aa");
   ASSERT_EQ(a["aa"], "");
 }
 
-TEST(ParseGetQuery, Single)
+TEST(ParseGetArguments, Single)
 {
+  HttpHandler::GetArguments b;
+  HttpHandler::ParseGetArguments(b, "aaa=baaa");
+
   HttpHandler::Arguments a;
-  HttpHandler::ParseGetQuery(a, "aaa=baaa");
+  HttpHandler::CompileGetArguments(a, b);
+
   ASSERT_EQ(1u, a.size());
   ASSERT_EQ(a["aaa"], "baaa");
 }
 
-TEST(ParseGetQuery, SingleEmpty)
+TEST(ParseGetArguments, SingleEmpty)
 {
+  HttpHandler::GetArguments b;
+  HttpHandler::ParseGetArguments(b, "aaa");
+
   HttpHandler::Arguments a;
-  HttpHandler::ParseGetQuery(a, "aaa");
+  HttpHandler::CompileGetArguments(a, b);
+
   ASSERT_EQ(1u, a.size());
   ASSERT_EQ(a["aaa"], "");
 }
 
+TEST(ParseGetQuery, Test1)
+{
+  UriComponents uri;
+  HttpHandler::GetArguments b;
+  HttpHandler::ParseGetQuery(uri, b, "/instances/test/world?aaa=baaa&bb=a&aa=c");
+
+  HttpHandler::Arguments a;
+  HttpHandler::CompileGetArguments(a, b);
+
+  ASSERT_EQ(3u, uri.size());
+  ASSERT_EQ("instances", uri[0]);
+  ASSERT_EQ("test", uri[1]);
+  ASSERT_EQ("world", uri[2]);
+  ASSERT_EQ(3u, a.size());
+  ASSERT_EQ(a["aaa"], "baaa");
+  ASSERT_EQ(a["bb"], "a");
+  ASSERT_EQ(a["aa"], "c");
+}
+
+TEST(ParseGetQuery, Test2)
+{
+  UriComponents uri;
+  HttpHandler::GetArguments b;
+  HttpHandler::ParseGetQuery(uri, b, "/instances/test/world");
+
+  HttpHandler::Arguments a;
+  HttpHandler::CompileGetArguments(a, b);
+
+  ASSERT_EQ(3u, uri.size());
+  ASSERT_EQ("instances", uri[0]);
+  ASSERT_EQ("test", uri[1]);
+  ASSERT_EQ("world", uri[2]);
+  ASSERT_EQ(0u, a.size());
+}
+
 TEST(Uri, SplitUriComponents)
 {
-  UriComponents c;
+  UriComponents c, d;
   Toolbox::SplitUriComponents(c, "/cou/hello/world");
   ASSERT_EQ(3u, c.size());
   ASSERT_EQ("cou", c[0]);
@@ -253,6 +305,37 @@
 }
 
 
+TEST(Uri, Truncate)
+{
+  UriComponents c, d;
+  Toolbox::SplitUriComponents(c, "/cou/hello/world");
+
+  Toolbox::TruncateUri(d, c, 0);
+  ASSERT_EQ(3u, d.size());
+  ASSERT_EQ("cou", d[0]);
+  ASSERT_EQ("hello", d[1]);
+  ASSERT_EQ("world", d[2]);
+
+  Toolbox::TruncateUri(d, c, 1);
+  ASSERT_EQ(2u, d.size());
+  ASSERT_EQ("hello", d[0]);
+  ASSERT_EQ("world", d[1]);
+
+  Toolbox::TruncateUri(d, c, 2);
+  ASSERT_EQ(1u, d.size());
+  ASSERT_EQ("world", d[0]);
+
+  Toolbox::TruncateUri(d, c, 3);
+  ASSERT_EQ(0u, d.size());
+
+  Toolbox::TruncateUri(d, c, 4);
+  ASSERT_EQ(0u, d.size());
+
+  Toolbox::TruncateUri(d, c, 5);
+  ASSERT_EQ(0u, d.size());
+}
+
+
 TEST(Uri, Child)
 {
   UriComponents c1;  Toolbox::SplitUriComponents(c1, "/hello/world");  
@@ -581,23 +664,58 @@
   ASSERT_EQ("", t[3]);
 }
 
+TEST(Toolbox, Enumerations)
+{
+  ASSERT_EQ(Encoding_Utf8, StringToEncoding(EnumerationToString(Encoding_Utf8)));
+  ASSERT_EQ(Encoding_Ascii, StringToEncoding(EnumerationToString(Encoding_Ascii)));
+  ASSERT_EQ(Encoding_Latin1, StringToEncoding(EnumerationToString(Encoding_Latin1)));
+  ASSERT_EQ(Encoding_Latin2, StringToEncoding(EnumerationToString(Encoding_Latin2)));
+  ASSERT_EQ(Encoding_Latin3, StringToEncoding(EnumerationToString(Encoding_Latin3)));
+  ASSERT_EQ(Encoding_Latin4, StringToEncoding(EnumerationToString(Encoding_Latin4)));
+  ASSERT_EQ(Encoding_Latin5, StringToEncoding(EnumerationToString(Encoding_Latin5)));
+  ASSERT_EQ(Encoding_Cyrillic, StringToEncoding(EnumerationToString(Encoding_Cyrillic)));
+  ASSERT_EQ(Encoding_Arabic, StringToEncoding(EnumerationToString(Encoding_Arabic)));
+  ASSERT_EQ(Encoding_Greek, StringToEncoding(EnumerationToString(Encoding_Greek)));
+  ASSERT_EQ(Encoding_Hebrew, StringToEncoding(EnumerationToString(Encoding_Hebrew)));
+  ASSERT_EQ(Encoding_Japanese, StringToEncoding(EnumerationToString(Encoding_Japanese)));
+  ASSERT_EQ(Encoding_Chinese, StringToEncoding(EnumerationToString(Encoding_Chinese)));
+  ASSERT_EQ(Encoding_Thai, StringToEncoding(EnumerationToString(Encoding_Thai)));
+
+  ASSERT_EQ(ResourceType_Patient, StringToResourceType(EnumerationToString(ResourceType_Patient)));
+  ASSERT_EQ(ResourceType_Study, StringToResourceType(EnumerationToString(ResourceType_Study)));
+  ASSERT_EQ(ResourceType_Series, StringToResourceType(EnumerationToString(ResourceType_Series)));
+  ASSERT_EQ(ResourceType_Instance, StringToResourceType(EnumerationToString(ResourceType_Instance)));
+
+  ASSERT_EQ(ImageFormat_Png, StringToImageFormat(EnumerationToString(ImageFormat_Png)));
+}
+
 
 
 #if defined(__linux)
 #include <endian.h>
+#elif defined(__FreeBSD__)
+#include <machine/endian.h>
 #endif
 
+
 TEST(Toolbox, Endianness)
 {
   // Parts of this test come from Adam Conrad
   // http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=728822#5
 
-#if defined(_WIN32)
+
+  /**
+   * Windows and OS X are assumed to always little-endian.
+   **/
+  
+#if defined(_WIN32) || defined(__APPLE__)
   ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness());
 
-#elif defined(__APPLE__)
-  ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness());
 
+  /**
+   * Linux.
+   **/
+  
 #elif defined(__linux) || defined(__FreeBSD_kernel__)
 
 #if !defined(__BYTE_ORDER)
@@ -610,12 +728,67 @@
   ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness());
 #  endif
 
+  
+  /**
+   * FreeBSD.
+   **/
+  
+#elif defined(__FreeBSD__)
+#  if _BYTE_ORDER == _BIG_ENDIAN
+   ASSERT_EQ(Endianness_Big, Toolbox::DetectEndianness());
+#  else // _LITTLE_ENDIAN
+   ASSERT_EQ(Endianness_Little, Toolbox::DetectEndianness());
+#  endif
+
 #else
 #error Support your platform here
 #endif
 }
 
 
+#if ORTHANC_PUGIXML_ENABLED == 1
+TEST(Toolbox, Xml)
+{
+  Json::Value a;
+  a["hello"] = "world";
+  a["42"] = 43;
+  a["b"] = Json::arrayValue;
+  a["b"].append("test");
+  a["b"].append("test2");
+
+  std::string s;
+  Toolbox::JsonToXml(s, a);
+
+  std::cout << s;
+}
+#endif
+
+
+#if !defined(_WIN32)
+TEST(Toolbox, ExecuteSystemCommand)
+{
+  std::vector<std::string> args(2);
+  args[0] = "Hello";
+  args[1] = "World";
+
+  Toolbox::ExecuteSystemCommand("echo", args);
+}
+#endif
+
+
+TEST(Toolbox, IsInteger)
+{
+  ASSERT_TRUE(Toolbox::IsInteger("00236"));
+  ASSERT_TRUE(Toolbox::IsInteger("-0042"));
+  ASSERT_TRUE(Toolbox::IsInteger("0"));
+  ASSERT_TRUE(Toolbox::IsInteger("-0"));
+
+  ASSERT_FALSE(Toolbox::IsInteger(""));
+  ASSERT_FALSE(Toolbox::IsInteger("42a"));
+  ASSERT_FALSE(Toolbox::IsInteger("42-"));
+}
+
+
 int main(int argc, char **argv)
 {
   // Initialize Google's logging library.
--- a/UnitTestsSources/Versions.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,133 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include <stdint.h>
-#include <math.h>
-#include <png.h>
-#include <ctype.h>
-#include <zlib.h>
-#include <curl/curl.h>
-#include <boost/version.hpp>
-#include <sqlite3.h>
-#include <lua.h>
-#include <openssl/opensslv.h>
-
-
-TEST(Versions, Zlib)
-{
-  ASSERT_STREQ(zlibVersion(), ZLIB_VERSION);
-}
-
-TEST(Versions, Curl)
-{
-  curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
-  ASSERT_STREQ(LIBCURL_VERSION, v->version);
-}
-
-TEST(Versions, Png)
-{
-  ASSERT_EQ(PNG_LIBPNG_VER_MAJOR * 10000 + PNG_LIBPNG_VER_MINOR * 100 + PNG_LIBPNG_VER_RELEASE,
-            png_access_version_number());
-}
-
-TEST(Versions, SQLite)
-{
-  // http://www.sqlite.org/capi3ref.html#sqlite3_libversion
-  assert(sqlite3_libversion_number() == SQLITE_VERSION_NUMBER );
-  assert(strcmp(sqlite3_sourceid(), SQLITE_SOURCE_ID) == 0);
-  assert(strcmp(sqlite3_libversion(), SQLITE_VERSION) == 0);
-
-  // Ensure that the SQLite version is above 3.7.0.
-  // "sqlite3_create_function_v2" is not defined in previous versions.
-  ASSERT_GE(SQLITE_VERSION_NUMBER, 3007000);
-}
-
-
-TEST(Versions, Lua)
-{
-  // Ensure that the Lua version is above 5.1.0. This version has
-  // introduced some API changes.
-  ASSERT_GE(LUA_VERSION_NUM, 501);
-}
-
-
-#if ORTHANC_STATIC == 1
-TEST(Versions, ZlibStatic)
-{
-  ASSERT_STREQ("1.2.7", zlibVersion());
-}
-
-TEST(Versions, BoostStatic)
-{
-  ASSERT_STREQ("1_55", BOOST_LIB_VERSION);
-}
-
-TEST(Versions, CurlStatic)
-{
-  curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
-  ASSERT_STREQ("7.26.0", v->version);
-}
-
-TEST(Versions, PngStatic)
-{
-  ASSERT_EQ(10512, png_access_version_number());
-  ASSERT_STREQ("1.5.12", PNG_LIBPNG_VER_STRING);
-}
-
-TEST(Versions, CurlSslStatic)
-{
-  curl_version_info_data * vinfo = curl_version_info(CURLVERSION_NOW);
-
-  // Check that SSL support is enabled when required
-  bool curlSupportsSsl = vinfo->features & CURL_VERSION_SSL;
-
-#if ORTHANC_SSL_ENABLED == 0
-  ASSERT_FALSE(curlSupportsSsl);
-#else
-  ASSERT_TRUE(curlSupportsSsl);
-#endif
-}
-
-TEST(Version, LuaStatic)
-{
-  ASSERT_STREQ("Lua 5.1.5", LUA_RELEASE);
-}
-
-TEST(Version, OpenSslStatic)
-{
-  ASSERT_EQ(0x1000107fL /* openssl-1.0.1g */, OPENSSL_VERSION_NUMBER);
-}
-
-#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/VersionsTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,133 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include <stdint.h>
+#include <math.h>
+#include <png.h>
+#include <ctype.h>
+#include <zlib.h>
+#include <curl/curl.h>
+#include <boost/version.hpp>
+#include <sqlite3.h>
+#include <lua.h>
+#include <openssl/opensslv.h>
+
+
+TEST(Versions, Zlib)
+{
+  ASSERT_STREQ(zlibVersion(), ZLIB_VERSION);
+}
+
+TEST(Versions, Curl)
+{
+  curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
+  ASSERT_STREQ(LIBCURL_VERSION, v->version);
+}
+
+TEST(Versions, Png)
+{
+  ASSERT_EQ(PNG_LIBPNG_VER_MAJOR * 10000 + PNG_LIBPNG_VER_MINOR * 100 + PNG_LIBPNG_VER_RELEASE,
+            png_access_version_number());
+}
+
+TEST(Versions, SQLite)
+{
+  // http://www.sqlite.org/capi3ref.html#sqlite3_libversion
+  assert(sqlite3_libversion_number() == SQLITE_VERSION_NUMBER );
+  assert(strcmp(sqlite3_sourceid(), SQLITE_SOURCE_ID) == 0);
+  assert(strcmp(sqlite3_libversion(), SQLITE_VERSION) == 0);
+
+  // Ensure that the SQLite version is above 3.7.0.
+  // "sqlite3_create_function_v2" is not defined in previous versions.
+  ASSERT_GE(SQLITE_VERSION_NUMBER, 3007000);
+}
+
+
+TEST(Versions, Lua)
+{
+  // Ensure that the Lua version is above 5.1.0. This version has
+  // introduced some API changes.
+  ASSERT_GE(LUA_VERSION_NUM, 501);
+}
+
+
+#if ORTHANC_STATIC == 1
+TEST(Versions, ZlibStatic)
+{
+  ASSERT_STREQ("1.2.7", zlibVersion());
+}
+
+TEST(Versions, BoostStatic)
+{
+  ASSERT_STREQ("1_55", BOOST_LIB_VERSION);
+}
+
+TEST(Versions, CurlStatic)
+{
+  curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
+  ASSERT_STREQ("7.26.0", v->version);
+}
+
+TEST(Versions, PngStatic)
+{
+  ASSERT_EQ(10512, png_access_version_number());
+  ASSERT_STREQ("1.5.12", PNG_LIBPNG_VER_STRING);
+}
+
+TEST(Versions, CurlSslStatic)
+{
+  curl_version_info_data * vinfo = curl_version_info(CURLVERSION_NOW);
+
+  // Check that SSL support is enabled when required
+  bool curlSupportsSsl = vinfo->features & CURL_VERSION_SSL;
+
+#if ORTHANC_SSL_ENABLED == 0
+  ASSERT_FALSE(curlSupportsSsl);
+#else
+  ASSERT_TRUE(curlSupportsSsl);
+#endif
+}
+
+TEST(Version, LuaStatic)
+{
+  ASSERT_STREQ("Lua 5.1.5", LUA_RELEASE);
+}
+
+TEST(Version, OpenSslStatic)
+{
+  ASSERT_EQ(0x1000107fL /* openssl-1.0.1g */, OPENSSL_VERSION_NUMBER);
+}
+
+#endif
--- a/UnitTestsSources/Zip.cpp	Wed Jun 25 15:34:40 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,166 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2014 Medical Physics Department, CHU of Liege,
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/OrthancException.h"
-#include "../Core/Compression/ZipWriter.h"
-#include "../Core/Compression/HierarchicalZipWriter.h"
-#include "../Core/Toolbox.h"
-
-
-using namespace Orthanc;
-
-TEST(ZipWriter, Basic)
-{
-  Orthanc::ZipWriter w;
-  w.SetOutputPath("UnitTestsResults/hello.zip");
-  w.Open();
-  w.OpenFile("world/hello");
-  w.Write("Hello world");
-}
-
-
-TEST(ZipWriter, Basic64)
-{
-  Orthanc::ZipWriter w;
-  w.SetOutputPath("UnitTestsResults/hello64.zip");
-  w.SetZip64(true);
-  w.Open();
-  w.OpenFile("world/hello");
-  w.Write("Hello world");
-}
-
-
-TEST(ZipWriter, Exceptions)
-{
-  Orthanc::ZipWriter w;
-  ASSERT_THROW(w.Open(), Orthanc::OrthancException);
-  w.SetOutputPath("UnitTestsResults/hello3.zip");
-  w.Open();
-  ASSERT_THROW(w.Write("hello world"), Orthanc::OrthancException);
-}
-
-
-
-
-
-namespace Orthanc
-{
-  // The namespace is necessary
-  // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members
-
-  TEST(HierarchicalZipWriter, Index)
-  {
-    HierarchicalZipWriter::Index i;
-    ASSERT_EQ("hello", i.OpenFile("hello"));
-    ASSERT_EQ("hello-2", i.OpenFile("hello"));
-    ASSERT_EQ("coucou", i.OpenFile("coucou"));
-    ASSERT_EQ("hello-3", i.OpenFile("hello"));
-
-    i.OpenDirectory("coucou");
-
-    ASSERT_EQ("coucou-2/world", i.OpenFile("world"));
-    ASSERT_EQ("coucou-2/world-2", i.OpenFile("world"));
-
-    i.OpenDirectory("world");
-  
-    ASSERT_EQ("coucou-2/world-3/hello", i.OpenFile("hello"));
-    ASSERT_EQ("coucou-2/world-3/hello-2", i.OpenFile("hello"));
-
-    i.CloseDirectory();
-
-    ASSERT_EQ("coucou-2/world-4", i.OpenFile("world"));
-
-    i.CloseDirectory();
-
-    ASSERT_EQ("coucou-3", i.OpenFile("coucou"));
-
-    ASSERT_THROW(i.CloseDirectory(), OrthancException);
-  }
-
-
-  TEST(HierarchicalZipWriter, Filenames)
-  {
-    ASSERT_EQ("trE hell", HierarchicalZipWriter::Index::KeepAlphanumeric("    ÊtrE hellô  "));
-
-    // The "^" character is considered as a space in DICOM
-    ASSERT_EQ("Hel lo world", HierarchicalZipWriter::Index::KeepAlphanumeric("    Hel^^  ^\r\n\t^^lo  \t  <world>  "));
-  }
-}
-
-
-TEST(HierarchicalZipWriter, Basic)
-{
-  static const std::string SPACES = "                             ";
-
-  HierarchicalZipWriter w("UnitTestsResults/hello2.zip");
-
-  w.SetCompressionLevel(0);
-
-  // Inside "/"
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello\n");
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello-2\n");
-  w.OpenDirectory("hello");
-
-  // Inside "/hello-3"
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello\n");
-  w.OpenDirectory("hello");
-
-  w.SetCompressionLevel(9);
-
-  // Inside "/hello-3/hello-2"
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello\n");
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello-2\n");
-  w.CloseDirectory();
-
-  // Inside "/hello-3"
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello-3\n");
-
-  /**
-
-     TO CHECK THE CONTENT OF THE "hello2.zip" FILE:
-
-     # unzip -v hello2.zip 
-
-     => There must be 6 files. The first 3 files must have a negative
-     compression ratio.
-
-  **/
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTestsSources/ZipTests.cpp	Thu May 21 16:58:30 2015 +0200
@@ -0,0 +1,188 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/OrthancException.h"
+#include "../Core/Compression/ZipWriter.h"
+#include "../Core/Compression/HierarchicalZipWriter.h"
+#include "../Core/Toolbox.h"
+
+
+using namespace Orthanc;
+
+TEST(ZipWriter, Basic)
+{
+  Orthanc::ZipWriter w;
+  w.SetOutputPath("UnitTestsResults/hello.zip");
+  w.Open();
+  w.OpenFile("world/hello");
+  w.Write("Hello world");
+}
+
+
+TEST(ZipWriter, Basic64)
+{
+  Orthanc::ZipWriter w;
+  w.SetOutputPath("UnitTestsResults/hello64.zip");
+  w.SetZip64(true);
+  w.Open();
+  w.OpenFile("world/hello");
+  w.Write("Hello world");
+}
+
+
+TEST(ZipWriter, Exceptions)
+{
+  Orthanc::ZipWriter w;
+  ASSERT_THROW(w.Open(), Orthanc::OrthancException);
+  w.SetOutputPath("UnitTestsResults/hello3.zip");
+  w.Open();
+  ASSERT_THROW(w.Write("hello world"), Orthanc::OrthancException);
+}
+
+
+TEST(ZipWriter, Append)
+{
+  {
+    Orthanc::ZipWriter w;
+    w.SetAppendToExisting(false);
+    w.SetOutputPath("UnitTestsResults/append.zip");
+    w.Open();
+    w.OpenFile("world/hello");
+    w.Write("Hello world 1");
+  }
+
+  {
+    Orthanc::ZipWriter w;
+    w.SetAppendToExisting(true);
+    w.SetOutputPath("UnitTestsResults/append.zip");
+    w.Open();
+    w.OpenFile("world/appended");
+    w.Write("Hello world 2");
+  }
+}
+
+
+
+
+
+namespace Orthanc
+{
+  // The namespace is necessary
+  // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members
+
+  TEST(HierarchicalZipWriter, Index)
+  {
+    HierarchicalZipWriter::Index i;
+    ASSERT_EQ("hello", i.OpenFile("hello"));
+    ASSERT_EQ("hello-2", i.OpenFile("hello"));
+    ASSERT_EQ("coucou", i.OpenFile("coucou"));
+    ASSERT_EQ("hello-3", i.OpenFile("hello"));
+
+    i.OpenDirectory("coucou");
+
+    ASSERT_EQ("coucou-2/world", i.OpenFile("world"));
+    ASSERT_EQ("coucou-2/world-2", i.OpenFile("world"));
+
+    i.OpenDirectory("world");
+  
+    ASSERT_EQ("coucou-2/world-3/hello", i.OpenFile("hello"));
+    ASSERT_EQ("coucou-2/world-3/hello-2", i.OpenFile("hello"));
+
+    i.CloseDirectory();
+
+    ASSERT_EQ("coucou-2/world-4", i.OpenFile("world"));
+
+    i.CloseDirectory();
+
+    ASSERT_EQ("coucou-3", i.OpenFile("coucou"));
+
+    ASSERT_THROW(i.CloseDirectory(), OrthancException);
+  }
+
+
+  TEST(HierarchicalZipWriter, Filenames)
+  {
+    ASSERT_EQ("trE hell", HierarchicalZipWriter::Index::KeepAlphanumeric("    ÊtrE hellô  "));
+
+    // The "^" character is considered as a space in DICOM
+    ASSERT_EQ("Hel lo world", HierarchicalZipWriter::Index::KeepAlphanumeric("    Hel^^  ^\r\n\t^^lo  \t  <world>  "));
+  }
+}
+
+
+TEST(HierarchicalZipWriter, Basic)
+{
+  static const std::string SPACES = "                             ";
+
+  HierarchicalZipWriter w("UnitTestsResults/hello2.zip");
+
+  w.SetCompressionLevel(0);
+
+  // Inside "/"
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello\n");
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello-2\n");
+  w.OpenDirectory("hello");
+
+  // Inside "/hello-3"
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello\n");
+  w.OpenDirectory("hello");
+
+  w.SetCompressionLevel(9);
+
+  // Inside "/hello-3/hello-2"
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello\n");
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello-2\n");
+  w.CloseDirectory();
+
+  // Inside "/hello-3"
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello-3\n");
+
+  /**
+
+     TO CHECK THE CONTENT OF THE "hello2.zip" FILE:
+
+     # unzip -v hello2.zip 
+
+     => There must be 6 files. The first 3 files must have a negative
+     compression ratio.
+
+  **/
+}