changeset 971:509e146c3cb3 plugins

integration mainline->plugins
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 30 Jun 2014 13:36:01 +0200
parents 743a75b14bef (current diff) 1a3817d84f39 (diff)
children ab6475e33473
files CMakeLists.txt Core/Enumerations.h Core/Toolbox.cpp Core/Toolbox.h NEWS OrthancServer/ParsedDicomFile.cpp 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/UnitTestsMain.cpp UnitTestsSources/Versions.cpp UnitTestsSources/Zip.cpp
diffstat 44 files changed, 3832 insertions(+), 2680 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Wed Jun 25 15:37:48 2014 +0200
+++ b/CMakeLists.txt	Mon Jun 30 13:36:01 2014 +0200
@@ -84,6 +84,7 @@
   Core/HttpServer/MongooseServer.cpp
   Core/HttpServer/HttpFileSender.cpp
   Core/HttpServer/FilesystemHttpSender.cpp
+  Core/RestApi/RestApiHierarchy.cpp
   Core/RestApi/RestApiPath.cpp
   Core/RestApi/RestApiOutput.cpp
   Core/RestApi/RestApi.cpp
@@ -156,22 +157,23 @@
 
 
 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
   )
 
--- a/Core/DicomFormat/DicomTag.cpp	Wed Jun 25 15:37:48 2014 +0200
+++ b/Core/DicomFormat/DicomTag.cpp	Mon Jun 30 13:36:01 2014 +0200
@@ -116,4 +116,132 @@
 
     return "";
   }
+
+
+  void DicomTag::GetTagsForModule(std::set<DicomTag>& target,
+                                  ResourceType module)
+  {
+    // REFERENCE: 11_03pu.pdf, DICOM PS 3.3 2011 - Information Object Definitions
+    target.clear();
+
+    switch (module)
+    {
+      case ResourceType_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 ResourceType_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 ResourceType_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 ResourceType_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);
+    }
+  }
 }
--- a/Core/DicomFormat/DicomTag.h	Wed Jun 25 15:37:48 2014 +0200
+++ b/Core/DicomFormat/DicomTag.h	Mon Jun 30 13:36:01 2014 +0200
@@ -33,8 +33,10 @@
 #pragma once
 
 #include <string>
+#include <set>
 #include <stdint.h>
 
+#include "../Enumerations.h"
 
 namespace Orthanc
 {
@@ -81,6 +83,9 @@
     std::string Format() const;
 
     friend std::ostream& operator<< (std::ostream& o, const DicomTag& tag);
+
+    static void GetTagsForModule(std::set<DicomTag>& target,
+                                 ResourceType module);
   };
 
   // Aliases for the most useful tags
--- a/Core/Enumerations.h	Wed Jun 25 15:37:48 2014 +0200
+++ b/Core/Enumerations.h	Mon Jun 30 13:36:01 2014 +0200
@@ -231,6 +231,7 @@
 
   enum Encoding
   {
+    Encoding_Ascii,
     Encoding_Utf8,
     Encoding_Latin1
   };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiHierarchy.cpp	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,412 @@
+/**
+ * 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 "RestApiHierarchy.h"
+
+#include <cassert>
+
+namespace Orthanc
+{
+  bool RestApiHierarchy::Handlers::IsEmpty() const
+  {
+    return (getHandlers_.empty() &&
+            postHandlers_.empty() &&
+            putHandlers_.empty() &&
+            deleteHandlers_.empty());
+  }
+
+
+  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;
+    }
+  }
+
+
+  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::LookupHandler(RestApiPath::Components& components,
+                                       const UriComponents& uri,
+                                       ResourceCallback callback,
+                                       size_t level,
+                                       void* call)
+  {
+    assert(uri.size() >= level);
+    UriComponents trailing;
+
+    // Look for an exact match on the resource of interest
+    if (uri.size() == level)
+    {
+      if (!handlers_.IsEmpty() &&
+          callback(handlers_, uri, components, trailing, call))
+      {
+        return true;
+      }
+    }
+
+
+    // 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->LookupHandler(components, uri, callback, level + 1, call))
+      {
+        return true;
+      }
+    }
+
+
+    // Try and go down in the hierarchy, using wildcard rules for children
+    for (child = wildcardChildren_.begin();
+         child != wildcardChildren_.end(); child++)
+    {
+      RestApiPath::Components subComponents = components;
+      subComponents[child->first] = uri[level];
+
+      if (child->second->LookupHandler(components, uri, callback, level + 1, call))
+      {
+        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 (callback(universalHandlers_, uri, components, trailing, call))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  bool RestApiHierarchy::GetDirectory(Json::Value& result,
+                                      const UriComponents& uri,
+                                      size_t level)
+  {
+    if (uri.size() == level)
+    {
+      if (!handlers_.HasGet() && 
+          universalHandlers_.IsEmpty() &&
+          wildcardChildren_.size() == 0)
+      {
+        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;
+  }
+                       
+
+  bool RestApiHierarchy::GetCallback(Handlers& handlers,
+                                     const UriComponents& uri,
+                                     const RestApiPath::Components& components,
+                                     const UriComponents& trailing,
+                                     void* call)
+  {
+    for (Handlers::GetHandlers::iterator
+           it = handlers.getHandlers_.begin(); 
+         it != handlers.getHandlers_.end(); it++)
+    {
+      // TODO RETURN BOOL
+
+      (*it) (*reinterpret_cast<RestApi::GetCall*>(call));
+      return true;
+    }
+
+    return false;
+  }
+
+
+  bool RestApiHierarchy::PostCallback(Handlers& handlers,
+                                      const UriComponents& uri,
+                                      const RestApiPath::Components& components,
+                                      const UriComponents& trailing,
+                                      void* call)
+  {
+    for (Handlers::PostHandlers::iterator
+           it = handlers.postHandlers_.begin(); 
+         it != handlers.postHandlers_.end(); it++)
+    {
+      // TODO RETURN BOOL
+
+      (*it) (*reinterpret_cast<RestApi::PostCall*>(call));
+      return true;
+    }
+
+    return false;
+  }
+
+
+  bool RestApiHierarchy::PutCallback(Handlers& handlers,
+                                     const UriComponents& uri,
+                                     const RestApiPath::Components& components,
+                                     const UriComponents& trailing,
+                                     void* call)
+  {
+    for (Handlers::PutHandlers::iterator
+           it = handlers.putHandlers_.begin(); 
+         it != handlers.putHandlers_.end(); it++)
+    {
+      // TODO RETURN BOOL
+
+      (*it) (*reinterpret_cast<RestApi::PutCall*>(call));
+      return true;
+    }
+
+    return false;
+  }
+
+
+  bool RestApiHierarchy::DeleteCallback(Handlers& handlers,
+                                        const UriComponents& uri,
+                                        const RestApiPath::Components& components,
+                                        const UriComponents& trailing,
+                                        void* call)
+  {
+    for (Handlers::DeleteHandlers::iterator
+           it = handlers.deleteHandlers_.begin(); 
+         it != handlers.deleteHandlers_.end(); it++)
+    {
+      // TODO RETURN BOOL
+
+      (*it) (*reinterpret_cast<RestApi::DeleteCall*>(call));
+      return true;
+    }
+
+    return false;
+  }
+
+
+  RestApiHierarchy::~RestApiHierarchy()
+  {
+    DeleteChildren(children_);
+    DeleteChildren(wildcardChildren_);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApi::GetHandler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApi::PutHandler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApi::PostHandler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApi::DeleteHandler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::CreateSiteMap(Json::Value& target) const
+  {
+    if (children_.size() == 0)
+    {
+      std::string s = " ";
+      if (handlers_.getHandlers_.size() != 0)
+      {
+        s += "GET ";
+      }
+
+      if (handlers_.postHandlers_.size() != 0)
+      {
+        s += "POST ";
+      }
+
+      if (handlers_.putHandlers_.size() != 0)
+      {
+        s += "PUT ";
+      }
+
+      if (handlers_.deleteHandlers_.size() != 0)
+      {
+        s += "DELETE ";
+      }
+
+      target = s;
+    }
+    else
+    {
+      target = Json::objectValue;
+      
+      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::Handle(RestApi::GetCall& call,
+                                const UriComponents& uri)
+  {
+    RestApiPath::Components components;
+    return LookupHandler(components, uri, GetCallback, 0, &call);
+  }    
+
+  bool RestApiHierarchy::Handle(RestApi::PutCall& call,
+                                const UriComponents& uri)
+  {
+    RestApiPath::Components components;
+    return LookupHandler(components, uri, PutCallback, 0, &call);
+  }    
+
+  bool RestApiHierarchy::Handle(RestApi::PostCall& call,
+                                const UriComponents& uri)
+  {
+    RestApiPath::Components components;
+    return LookupHandler(components, uri, PostCallback, 0, &call);
+  }    
+
+  bool RestApiHierarchy::Handle(RestApi::DeleteCall& call,
+                                const UriComponents& uri)
+  {
+    RestApiPath::Components components;
+    return LookupHandler(components, uri, DeleteCallback, 0, &call);
+  }    
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/RestApi/RestApiHierarchy.h	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,178 @@
+/**
+ * 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 "RestApi.h"
+
+#include <list>
+
+namespace Orthanc
+{
+  class RestApiHierarchy
+  {
+  private:
+    struct Handlers
+    {
+      typedef std::list<RestApi::GetHandler>  GetHandlers;
+      typedef std::list<RestApi::PostHandler>  PostHandlers;
+      typedef std::list<RestApi::PutHandler>  PutHandlers;
+      typedef std::list<RestApi::DeleteHandler>  DeleteHandlers;
+
+      GetHandlers  getHandlers_;
+      PostHandlers  postHandlers_;
+      PutHandlers  putHandlers_;
+      DeleteHandlers  deleteHandlers_;
+
+      bool HasGet() const
+      {
+        return getHandlers_.size() > 0;
+      }
+
+      void Register(RestApi::GetHandler handler)
+      {
+        getHandlers_.push_back(handler);
+      }
+
+      void Register(RestApi::PutHandler handler)
+      {
+        putHandlers_.push_back(handler);
+      }
+
+      void Register(RestApi::PostHandler handler)
+      {
+        postHandlers_.push_back(handler);
+      }
+
+      void Register(RestApi::DeleteHandler handler)
+      {
+        deleteHandlers_.push_back(handler);
+      }
+
+      bool IsEmpty() const;
+    };
+
+
+    typedef std::map<std::string, RestApiHierarchy*>  Children;
+    typedef bool (*ResourceCallback) (Handlers& handlers,
+                                      const UriComponents& uri,
+                                      const RestApiPath::Components& components,
+                                      const UriComponents& trailing,
+                                      void* call);
+
+    Handlers  handlers_;
+    Children  children_;
+    Children  wildcardChildren_;
+    Handlers  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 LookupHandler(RestApiPath::Components& components,
+                       const UriComponents& uri,
+                       ResourceCallback callback,
+                       size_t level,
+                       void* call);
+
+    bool GetDirectory(Json::Value& result,
+                      const UriComponents& uri,
+                      size_t level);
+                     
+    static bool GetCallback(Handlers& handlers,
+                            const UriComponents& uri,
+                            const RestApiPath::Components& components,
+                            const UriComponents& trailing,
+                            void* call);
+
+    static bool PostCallback(Handlers& handlers,
+                             const UriComponents& uri,
+                             const RestApiPath::Components& components,
+                             const UriComponents& trailing,
+                             void* call);
+
+    static bool PutCallback(Handlers& handlers,
+                            const UriComponents& uri,
+                            const RestApiPath::Components& components,
+                            const UriComponents& trailing,
+                            void* call);
+
+    static bool DeleteCallback(Handlers& handlers,
+                               const UriComponents& uri,
+                               const RestApiPath::Components& components,
+                               const UriComponents& trailing,
+                               void* call);
+                       
+
+  public:
+    ~RestApiHierarchy();
+
+    void Register(const std::string& uri,
+                  RestApi::GetHandler handler);
+
+    void Register(const std::string& uri,
+                  RestApi::PutHandler handler);
+
+    void Register(const std::string& uri,
+                  RestApi::PostHandler handler);
+
+    void Register(const std::string& uri,
+                  RestApi::DeleteHandler handler);
+
+    void CreateSiteMap(Json::Value& target) const;
+
+    bool GetDirectory(Json::Value& result,
+                      const UriComponents& uri)
+    {
+      return GetDirectory(result, uri, 0);
+    }
+
+    bool Handle(RestApi::GetCall& call,
+                const UriComponents& uri);
+
+    bool Handle(RestApi::PutCall& call,
+                const UriComponents& uri);
+
+    bool Handle(RestApi::PostCall& call,
+                const UriComponents& uri);
+
+    bool Handle(RestApi::DeleteCall& call,
+                const UriComponents& uri);
+  };
+}
--- a/Core/RestApi/RestApiPath.cpp	Wed Jun 25 15:37:48 2014 +0200
+++ b/Core/RestApi/RestApiPath.cpp	Mon Jun 30 13:36:01 2014 +0200
@@ -33,6 +33,8 @@
 #include "../PrecompiledHeaders.h"
 #include "RestApiPath.h"
 
+#include "../OrthancException.h"
+
 #include <cassert>
 
 namespace Orthanc
@@ -89,6 +91,8 @@
                           UriComponents& trailing,
                           const UriComponents& uri) const
   {
+    assert(uri_.size() == components_.size());
+
     if (uri.size() < uri_.size())
     {
       return false;
@@ -135,4 +139,42 @@
     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:37:48 2014 +0200
+++ b/Core/RestApi/RestApiPath.h	Mon Jun 30 13:36:01 2014 +0200
@@ -59,5 +59,22 @@
                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;
+
   };
 }
--- a/Core/Toolbox.cpp	Wed Jun 25 15:37:48 2014 +0200
+++ b/Core/Toolbox.cpp	Mon Jun 30 13:36:01 2014 +0200
@@ -284,6 +284,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)
   {
@@ -505,12 +527,15 @@
         // Already in UTF-8: No conversion is required
         return source;
 
+      case Encoding_Ascii:
+        return ConvertToAscii(source);;
+
       case Encoding_Latin1:
         encoding = "ISO-8859-1";
         break;
 
       default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancException(ErrorCode_NotImplemented);
     }
 
     try
--- a/Core/Toolbox.h	Wed Jun 25 15:37:48 2014 +0200
+++ b/Core/Toolbox.h	Mon Jun 30 13:36:01 2014 +0200
@@ -73,6 +73,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);
 
--- a/DarwinCompilation.txt	Wed Jun 25 15:37:48 2014 +0200
+++ b/DarwinCompilation.txt	Mon Jun 30 13:36:01 2014 +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 ~/Orthanc
 
 NB: Adapt the value of "CMAKE_OSX_DEPLOYMENT_TARGET" with respect to
 your version of XCode.
--- a/NEWS	Wed Jun 25 15:37:48 2014 +0200
+++ b/NEWS	Mon Jun 30 13:36:01 2014 +0200
@@ -3,6 +3,8 @@
 
 * Introduction of the Orthanc Plugin SDK
 * Official support of OS X (Darwin)
+* 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
 
--- a/OrthancServer/FromDcmtkBridge.cpp	Wed Jun 25 15:37:48 2014 +0200
+++ b/OrthancServer/FromDcmtkBridge.cpp	Mon Jun 30 13:36:01 2014 +0200
@@ -61,6 +61,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>
@@ -115,8 +116,44 @@
             GetCharValue(c[3]));
   }
 
+
+  Encoding FromDcmtkBridge::DetectEncoding(DcmDataset& dataset)
+  {
+    // By default, assume UTF-8 encoding (as in dcm2xml.cc)
+    Encoding encoding = Encoding_Utf8;
+
+    OFString tmp;
+    if (dataset.findAndGetOFString(DCM_SpecificCharacterSet, tmp).good())
+    {
+      std::string characterSet = Toolbox::StripSpaces(std::string(tmp.c_str()));
+
+      // TODO Add more encodings
+
+      if (characterSet == "ISO_IR 6" ||
+          characterSet == "ISO_IR 192")
+      {
+        encoding = Encoding_Utf8;
+      }
+      else if (characterSet == "ISO_IR 100")
+      {
+        encoding = Encoding_Latin1;
+      }
+      else if (!characterSet.empty())
+      {
+        LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet;
+        // Fallback to ASCII (remove all special characters)
+        encoding = Encoding_Ascii;
+      }
+    }
+
+    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,7 +162,7 @@
       {
         target.SetValue(element->getTag().getGTag(),
                         element->getTag().getETag(),
-                        ConvertLeafElement(*element));
+                        ConvertLeafElement(*element, encoding));
       }
     }
   }
@@ -137,7 +174,8 @@
   }
 
 
-  DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element)
+  DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element,
+                                                  Encoding encoding)
   {
     if (!element.isLeaf())
     {
@@ -151,7 +189,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
@@ -313,25 +351,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 +397,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 +434,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 +448,7 @@
                                DcmDataset& dataset,
                                unsigned int maxStringLength)
   {
-    StoreItem(root, dataset, maxStringLength);
+    StoreItem(root, dataset, maxStringLength, DetectEncoding(dataset));
   }
 
 
--- a/OrthancServer/FromDcmtkBridge.h	Wed Jun 25 15:37:48 2014 +0200
+++ b/OrthancServer/FromDcmtkBridge.h	Mon Jun 30 13:36:01 2014 +0200
@@ -44,11 +44,14 @@
   class FromDcmtkBridge
   {
   public:
+    static Encoding DetectEncoding(DcmDataset& dataset);
+
     static void Convert(DicomMap& target, DcmDataset& dataset);
 
     static DicomTag GetTag(const DcmElement& element);
 
-    static DicomValue* ConvertLeafElement(DcmElement& element);
+    static DicomValue* ConvertLeafElement(DcmElement& element,
+                                          Encoding encoding);
 
     static void ToJson(Json::Value& target, 
                        DcmDataset& dataset,
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed Jun 25 15:37:48 2014 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Mon Jun 30 13:36:01 2014 +0200
@@ -153,6 +153,21 @@
     }
   }
 
+
+  static void GetInstanceTagsBis(RestApi::GetCall& call)
+  {
+    bool simplify = call.HasArgument("simplify");
+
+    if (simplify)
+    {
+      GetInstanceTags<true>(call);
+    }
+    else
+    {
+      GetInstanceTags<false>(call);
+    }
+  }
+
   
   static void ListFrames(RestApi::GetCall& call)
   {
@@ -591,6 +606,160 @@
 
 
 
+  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(RestApi::GetCall& 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);
+      }
+    }
+  }
+
+
+  template <enum ResourceType resourceType>
+  static void GetModule(RestApi::GetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    std::string publicId = call.GetUriComponent("id", "");
+    bool simplify = call.HasArgument("simplify");
+
+    typedef std::set<DicomTag> Module;
+    Module module;
+    DicomTag::GetTagsForModule(module, resourceType);
+
+    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 (Module::const_iterator it = module.begin(); it != module.end(); it++)
+    {
+      std::string s = it->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);
+    }
+  }
+
+
   void OrthancRestApi::RegisterResources()
   {
     Register("/instances", ListResources<ResourceType_Instance>);
@@ -612,9 +781,18 @@
     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>);
+    Register("/patients/{id}/module", GetModule<ResourceType_Patient>);
+    Register("/series/{id}/module", GetModule<ResourceType_Series>);
+    Register("/studies/{id}/module", GetModule<ResourceType_Study>);
+
     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);
 
--- a/OrthancServer/ParsedDicomFile.cpp	Wed Jun 25 15:37:48 2014 +0200
+++ b/OrthancServer/ParsedDicomFile.cpp	Mon Jun 30 13:36:01 2014 +0200
@@ -146,6 +146,7 @@
   struct ParsedDicomFile::PImpl
   {
     std::auto_ptr<DcmFileFormat> file_;
+    Encoding encoding_;
   };
 
 
@@ -170,6 +171,8 @@
     }
     pimpl_->file_->loadAllDataIntoMemory();
     pimpl_->file_->transferEnd();
+
+    pimpl_->encoding_ = FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset());
   }
 
 
@@ -872,7 +875,7 @@
       return false;
     }
 
-    std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(*element));
+    std::auto_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement(*element, pimpl_->encoding_));
 
     if (v.get() == NULL)
     {
@@ -887,7 +890,6 @@
   }
 
 
-
   DicomInstanceHasher ParsedDicomFile::GetHasher()
   {
     std::string patientId, studyUid, seriesUid, instanceUid;
@@ -904,98 +906,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,
@@ -1044,6 +954,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 +984,8 @@
     pimpl_(new PImpl)
   {
     pimpl_->file_.reset(dynamic_cast<DcmFileFormat*>(other.pimpl_->file_->clone()));
+
+    pimpl_->encoding_ = other.pimpl_->encoding_;
   }
 
 
@@ -1279,4 +1192,9 @@
     writer.WriteToMemory(result, accessor);
   }
 
+
+  Encoding ParsedDicomFile::GetEncoding() const
+  {
+    return pimpl_->encoding_;
+  }
 }
--- a/OrthancServer/ParsedDicomFile.h	Wed Jun 25 15:37:48 2014 +0200
+++ b/OrthancServer/ParsedDicomFile.h	Mon Jun 30 13:36:01 2014 +0200
@@ -104,6 +104,8 @@
     void ExtractPngImage(std::string& result,
                          unsigned int frame,
                          ImageExtractionMode mode);
+
+    Encoding GetEncoding() const;
   };
 
 }
--- a/UnitTestsSources/DicomMap.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,189 @@
+/**
+ * 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 "../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)
+{
+  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));
+}
+
+
+
+
+static void TestModule(ResourceType level)
+{
+  std::set<DicomTag> module, main;
+  DicomTag::GetTagsForModule(module, level);
+  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 = module.find(*it) != module.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);
+  TestModule(ResourceType_Study);
+  //TestModule(ResourceType_Series);   // TODO
+  TestModule(ResourceType_Instance);
+}
--- a/UnitTestsSources/FileStorage.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,228 @@
+/**
+ * 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);
+  */
+}
--- a/UnitTestsSources/FromDcmtk.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,147 @@
+/**
+ * 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");
+  }
+}
--- a/UnitTestsSources/JpegLossless.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,54 @@
+/**
+ * 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
--- a/UnitTestsSources/Lua.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,136 @@
+/**
+ * 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();
+  }
+}
--- a/UnitTestsSources/MemoryCache.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,230 @@
+/**
+ * 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_);
+}
--- a/UnitTestsSources/MultiThreading.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,276 @@
+/**
+ * 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);
+}
--- a/UnitTestsSources/Png.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,186 @@
+/**
+ * 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);
+      }
+    }
+  }
+}
--- a/UnitTestsSources/RestApi.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,265 @@
+/**
+ * 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"
+#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)
+{
+  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"));
+
+    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(RestApi::GetCall& 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);
+}
+
+
+static bool HandleGet(RestApiHierarchy& hierarchy, 
+                      const std::string& uri)
+{
+  UriComponents p;
+  Toolbox::SplitUriComponents(p, uri);
+  return hierarchy.Handle(*reinterpret_cast<RestApi::GetCall*>(NULL), p);
+}
+
+
+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_FALSE(GetDirectory(d, root, "/hello/world/test"));
+  ASSERT_FALSE(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:37:48 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:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,377 @@
+/**
+ * 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/SQLiteTests.cpp	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,334 @@
+/**
+ * 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/UnitTestsMain.cpp	Wed Jun 25 15:37:48 2014 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Mon Jun 30 13:36:01 2014 +0200
@@ -239,7 +239,7 @@
 
 TEST(Uri, SplitUriComponents)
 {
-  UriComponents c;
+  UriComponents c, d;
   Toolbox::SplitUriComponents(c, "/cou/hello/world");
   ASSERT_EQ(3u, c.size());
   ASSERT_EQ("cou", c[0]);
@@ -280,6 +280,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");  
--- a/UnitTestsSources/Versions.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,133 @@
+/**
+ * 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
--- a/UnitTestsSources/Zip.cpp	Wed Jun 25 15:37:48 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	Mon Jun 30 13:36:01 2014 +0200
@@ -0,0 +1,166 @@
+/**
+ * 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.
+
+  **/
+}