changeset 286:727a6d766dde

merge
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 13 Dec 2012 14:52:57 +0100
parents e5402c368b21 (current diff) 4031f73fe0e4 (diff)
children 471df5fecb1e
files CMakeLists.txt
diffstat 18 files changed, 882 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Mon Dec 10 17:49:14 2012 +0100
+++ b/CMakeLists.txt	Thu Dec 13 14:52:57 2012 +0100
@@ -99,6 +99,7 @@
   ${AUTOGENERATED_SOURCES}
   ${THIRD_PARTY_SOURCES}
 
+  Core/Cache/MemoryCache.cpp
   Core/ChunkedBuffer.cpp
   Core/Compression/BufferCompressor.cpp
   Core/Compression/ZlibCompressor.cpp
@@ -188,6 +189,7 @@
       UnitTests/Versions.cpp
       UnitTests/Zip.cpp
       UnitTests/FileStorage.cpp
+      UnitTests/MemoryCache.cpp
       UnitTests/main.cpp
       )
     target_link_libraries(UnitTests ServerLibrary CoreLibrary)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/CacheIndex.h	Thu Dec 13 14:52:57 2012 +0100
@@ -0,0 +1,250 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012 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 <list>
+#include <map>
+#include <boost/noncopyable.hpp>
+#include <cassert>
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+namespace Orthanc
+{
+  /**
+   * This class implements the index of a cache with least recently
+   * used (LRU) recycling policy. All the items of the cache index
+   * can be associated with a payload.
+   * Reference: http://stackoverflow.com/a/2504317
+   **/
+  template <typename T, typename Payload = NullType>
+  class CacheIndex : public boost::noncopyable
+  {
+  private:
+    typedef std::list< std::pair<T, Payload> >  Queue;
+    typedef std::map<T, typename Queue::iterator>  Index;
+
+    Index  index_;
+    Queue  queue_;
+
+    /**
+     * Internal method for debug builds to check whether the internal
+     * data structures are not corrupted.
+     **/
+    void CheckInvariants() const;
+
+  public:
+    /**
+     * Add a new element to the cache index, and make it the most
+     * recent element.
+     * \param id The ID of the element.
+     * \param payload The payload of the element.
+     **/
+    void Add(T id, Payload payload = Payload());
+
+    /**
+     * When accessing an element of the cache, this method tags the
+     * element as the most recently used.
+     * \param id The most recently accessed item.
+     **/
+    void TagAsMostRecent(T id);
+
+    /**
+     * Remove an element from the cache index.
+     * \param id The item to remove.
+     **/
+    Payload Invalidate(T id);
+
+    /**
+     * Get the oldest element in the cache and remove it.
+     * \return The oldest item.
+     **/
+    T RemoveOldest()
+    {
+      Payload p;
+      return RemoveOldest(p);
+    }
+
+    /**
+     * Get the oldest element in the cache, remove it and return the
+     * associated payload.
+     * \param payload Where to store the associated payload.
+     * \return The oldest item.
+     **/
+    T RemoveOldest(Payload& payload);
+
+    /**
+     * Check whether an element is contained in the cache.
+     * \param id The item.
+     * \return \c true iff the item is indexed by the cache.
+     **/
+    bool Contains(T id) const
+    {
+      return index_.find(id) != index_.end();
+    }
+
+    bool Contains(T id, Payload& payload) const
+    {
+      typename Index::const_iterator it = index_.find(id);
+      if (it == index_.end())
+      {
+        return false;
+      }
+      else
+      {
+        payload = it->second->second;
+        return true;
+      }
+    }
+
+    /**
+     * Return the number of elements in the cache.
+     * \return The number of elements.
+     **/
+    size_t GetSize() const
+    {
+      assert(index_.size() == queue_.size());
+      return queue_.size();
+    }
+
+    /**
+     * Check whether the cache index is empty.
+     * \return \c true iff the cache is empty.
+     **/
+    bool IsEmpty() const
+    {
+      return index_.empty();
+    }
+  };
+
+
+
+
+  /******************************************************************
+   ** Implementation of the template
+   ******************************************************************/
+
+  template <typename T, typename Payload>
+  void CacheIndex<T, Payload>::CheckInvariants() const
+  {
+#ifndef NDEBUG
+    assert(index_.size() == queue_.size());
+
+    for (typename Index::const_iterator 
+           it = index_.begin(); it != index_.end(); it++)
+    {
+      assert(it->second != queue_.end());
+      assert(it->second->first == it->first);
+    }
+#endif
+  }
+
+
+  template <typename T, typename Payload>
+  void CacheIndex<T, Payload>::Add(T id, Payload payload)
+  {
+    if (Contains(id))
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    queue_.push_front(std::make_pair(id, payload));
+    index_[id] = queue_.begin();
+
+    CheckInvariants();
+  }
+
+
+  template <typename T, typename Payload>
+  void CacheIndex<T, Payload>::TagAsMostRecent(T id)
+  {
+    if (!Contains(id))
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+
+    typename Index::iterator it = index_.find(id);
+    assert(it != index_.end());
+
+    std::pair<T, Payload> item = *(it->second);
+    
+    queue_.erase(it->second);
+    queue_.push_front(item);
+    index_[id] = queue_.begin();
+
+    CheckInvariants();
+  }
+
+
+  template <typename T, typename Payload>
+  Payload CacheIndex<T, Payload>::Invalidate(T id)
+  {
+    if (!Contains(id))
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+
+    typename Index::iterator it = index_.find(id);
+    assert(it != index_.end());
+
+    Payload payload = it->second->second;
+    queue_.erase(it->second);
+    index_.erase(it);
+
+    CheckInvariants();
+    return payload;
+  }
+
+
+  template <typename T, typename Payload>
+  T CacheIndex<T, Payload>::RemoveOldest(Payload& payload)
+  {
+    if (IsEmpty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    std::pair<T, Payload> item = queue_.back();
+    T oldest = item.first;
+    payload = item.second;
+
+    queue_.pop_back();
+    assert(index_.find(oldest) != index_.end());
+    index_.erase(oldest);
+
+    CheckInvariants();
+
+    return oldest;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/ICachePageProvider.h	Thu Dec 13 14:52:57 2012 +0100
@@ -0,0 +1,49 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012 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 <string>
+#include "../IDynamicObject.h"
+
+namespace Orthanc
+{
+  class ICachePageProvider
+  {
+  public:
+    virtual ~ICachePageProvider()
+    {
+    }
+
+    virtual IDynamicObject* Provide(const std::string& id) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/MemoryCache.cpp	Thu Dec 13 14:52:57 2012 +0100
@@ -0,0 +1,89 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012 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 "MemoryCache.h"
+
+namespace Orthanc
+{
+  MemoryCache::Page& MemoryCache::Load(const std::string& id)
+  {
+    // Reuse the cache entry if it already exists
+    Page* p = NULL;
+    if (index_.Contains(id, p))
+    {
+      assert(p != NULL);
+      index_.TagAsMostRecent(id);
+      return *p;
+    }
+
+    // The id is not in the cache yet. Make some room if the cache
+    // is full.
+    if (index_.GetSize() == cacheSize_)
+    {
+      index_.RemoveOldest(p);
+      delete p;
+    }
+
+    // Create a new cache page
+    std::auto_ptr<Page> result(new Page);
+    result->id_ = id;
+    result->content_.reset(provider_.Provide(id));
+
+    // Add the newly create page to the cache
+    p = result.release();
+    index_.Add(id, p);
+    return *p;
+  }
+
+  MemoryCache::MemoryCache(ICachePageProvider& provider,
+                           size_t cacheSize) : 
+    provider_(provider),
+    cacheSize_(cacheSize)
+  {
+  }
+
+  MemoryCache::~MemoryCache()
+  {
+    while (!index_.IsEmpty())
+    {
+      Page* element = NULL;
+      index_.RemoveOldest(element);
+      assert(element != NULL);
+      delete element;
+    }
+  }
+
+  IDynamicObject& MemoryCache::Access(const std::string& id)
+  {
+    return *Load(id).content_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/MemoryCache.h	Thu Dec 13 14:52:57 2012 +0100
@@ -0,0 +1,67 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012 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 <memory>
+#include "CacheIndex.h"
+#include "ICachePageProvider.h"
+
+namespace Orthanc
+{
+  /**
+   * WARNING: This class is NOT thread-safe.
+   **/
+  class MemoryCache
+  {
+  private:
+    struct Page
+    {
+      std::string id_;
+      std::auto_ptr<IDynamicObject> content_;
+    };
+
+    ICachePageProvider& provider_;
+    size_t cacheSize_;
+    CacheIndex<std::string, Page*>  index_;
+
+    Page& Load(const std::string& id);
+
+  public:
+    MemoryCache(ICachePageProvider& provider,
+                size_t cacheSize);
+
+    ~MemoryCache();
+
+    IDynamicObject& Access(const std::string& id);
+  };
+}
--- a/Core/Enumerations.h	Mon Dec 10 17:49:14 2012 +0100
+++ b/Core/Enumerations.h	Thu Dec 13 14:52:57 2012 +0100
@@ -47,6 +47,7 @@
     ErrorCode_NotEnoughMemory,
     ErrorCode_BadParameterType,
     ErrorCode_BadSequenceOfCalls,
+    ErrorCode_InexistentItem,
 
     // Specific error codes
     ErrorCode_UriSyntax,
--- a/Core/HttpServer/HttpOutput.h	Mon Dec 10 17:49:14 2012 +0100
+++ b/Core/HttpServer/HttpOutput.h	Thu Dec 13 14:52:57 2012 +0100
@@ -43,11 +43,6 @@
   private:
     void SendHeaderInternal(Orthanc_HttpStatus status);
 
-    void SendOkHeader(const char* contentType,
-                      bool hasContentLength,
-                      uint64_t contentLength,
-                      const char* contentFilename);
-
   public:
     virtual ~HttpOutput()
     {
@@ -55,6 +50,11 @@
 
     virtual void Send(const void* buffer, size_t length) = 0;
 
+    void SendOkHeader(const char* contentType,
+                      bool hasContentLength,
+                      uint64_t contentLength,
+                      const char* contentFilename);
+
     void SendCustomOkHeader(const std::string& customHeader);
 
     void SendString(const std::string& s);
--- a/Core/OrthancException.cpp	Mon Dec 10 17:49:14 2012 +0100
+++ b/Core/OrthancException.cpp	Thu Dec 13 14:52:57 2012 +0100
@@ -96,6 +96,9 @@
       case ErrorCode_FullStorage:
         return "The file storage is full";
 
+      case ErrorCode_InexistentItem:
+        return "Accessing an inexistent item";
+
       case ErrorCode_Custom:
       default:
         return "???";
--- a/Core/RestApi/RestApi.h	Mon Dec 10 17:49:14 2012 +0100
+++ b/Core/RestApi/RestApi.h	Thu Dec 13 14:52:57 2012 +0100
@@ -70,6 +70,11 @@
       {
         return *fullUri_;
       }
+    
+      const UriComponents& GetTrailingUri() const
+      {
+        return *trailing_;
+      }
 
       std::string GetUriComponent(const std::string& name,
                                   const std::string& defaultValue) const
--- a/Core/RestApi/RestApiOutput.h	Mon Dec 10 17:49:14 2012 +0100
+++ b/Core/RestApi/RestApiOutput.h	Thu Dec 13 14:52:57 2012 +0100
@@ -52,6 +52,16 @@
 
     ~RestApiOutput();
 
+    HttpOutput& GetLowLevelOutput()
+    {
+      return output_;
+    }
+
+    void MarkLowLevelOutputDone()
+    {
+      alreadySent_ = true;
+    }
+
     void AnswerFile(HttpFileSender& sender);
 
     void AnswerJson(const Json::Value& value);
--- a/Core/Toolbox.h	Mon Dec 10 17:49:14 2012 +0100
+++ b/Core/Toolbox.h	Thu Dec 13 14:52:57 2012 +0100
@@ -40,6 +40,10 @@
 {
   typedef std::vector<std::string> UriComponents;
 
+  class NullType
+  {
+  };
+
   namespace Toolbox
   {
     void ServerBarrier();
--- a/NEWS	Mon Dec 10 17:49:14 2012 +0100
+++ b/NEWS	Thu Dec 13 14:52:57 2012 +0100
@@ -3,6 +3,7 @@
 
 * Recycling of disk space
 * Protection of patients against recycling (also in Orthanc Explorer)
+* Raw access to the value of the DICOM tags in the REST API
 
 
 Version 0.3.1 (2012/12/05)
--- a/OrthancServer/FromDcmtkBridge.cpp	Mon Dec 10 17:49:14 2012 +0100
+++ b/OrthancServer/FromDcmtkBridge.cpp	Thu Dec 13 14:52:57 2012 +0100
@@ -63,6 +63,193 @@
 
 namespace Orthanc
 {
+  ParsedDicomFile::ParsedDicomFile(const std::string& content)
+  {
+    DcmInputBufferStream is;
+    if (content.size() > 0)
+    {
+      is.setBuffer(&content[0], content.size());
+    }
+    is.setEos();
+
+    file_.reset(new DcmFileFormat);
+    if (!file_->read(is).good())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  static void SendPathValueForDictionary(RestApiOutput& output,
+                                       DcmItem& dicom)
+  {
+    Json::Value v = Json::arrayValue;
+
+    for (unsigned long i = 0; i < dicom.card(); i++)
+    {
+      DcmElement* element = dicom.getElement(i);
+      if (element)
+      {
+        char buf[16];
+        sprintf(buf, "%04x-%04x", element->getTag().getGTag(), element->getTag().getETag());
+        v.append(buf);
+      }
+    }
+
+    output.AnswerJson(v);
+  }
+
+  static inline uint16_t GetCharValue(char c)
+  {
+    if (c >= '0' && c <= '9')
+      return c - '0';
+    else if (c >= 'a' && c <= 'f')
+      return c - 'a' + 10;
+    else if (c >= 'A' && c <= 'F')
+      return c - 'A' + 10;
+    else
+      return 0;
+  }
+
+  static inline uint16_t GetTagValue(const char* c)
+  {
+    return ((GetCharValue(c[0]) << 12) + 
+            (GetCharValue(c[1]) << 8) + 
+            (GetCharValue(c[2]) << 4) + 
+            GetCharValue(c[3]));
+  }
+
+  static bool ParseTagAndGroup(DcmTagKey& key,
+                               const std::string& tag)
+  {
+    if (tag.size() != 9 ||
+        !isxdigit(tag[0]) ||
+        !isxdigit(tag[1]) ||
+        !isxdigit(tag[2]) ||
+        !isxdigit(tag[3]) ||
+        tag[4] != '-' ||
+        !isxdigit(tag[5]) ||
+        !isxdigit(tag[6]) ||
+        !isxdigit(tag[7]) ||
+        !isxdigit(tag[8]))        
+    {
+      return false;
+    }
+
+    uint16_t group = GetTagValue(tag.c_str());
+    uint16_t element = GetTagValue(tag.c_str() + 5);
+
+    key = DcmTagKey(group, element);
+
+    return true;
+  }
+
+  static void SendPathValueForLeaf(RestApiOutput& output,
+                                   const std::string& tag,
+                                   DcmItem& dicom)
+  {
+    DcmTagKey k;
+    if (!ParseTagAndGroup(k, tag))
+    {
+      return;
+    }
+
+    DcmElement* element = NULL;
+    if (dicom.findAndGetElement(k, element).good() && element != NULL)
+    {
+      if (element->getVR() == EVR_SQ)
+      {
+        // This element is a sequence
+        Json::Value v = Json::arrayValue;
+        DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(*element);
+
+        for (unsigned long i = 0; i < sequence.card(); i++)
+        {
+          v.append(boost::lexical_cast<std::string>(i));
+        }
+
+        output.AnswerJson(v);
+      }
+      else
+      {
+        // This element is not a sequence
+        std::string buffer;
+        buffer.resize(65536);
+        Uint32 length = element->getLength();
+        Uint32 offset = 0;
+
+        output.GetLowLevelOutput().SendOkHeader("application/octet-stream", true, length, NULL);
+
+        while (offset < length)
+        {
+          Uint32 nbytes;
+          if (length - offset < buffer.size())
+          {
+            nbytes = length - offset;
+          }
+          else
+          {
+            nbytes = buffer.size();
+          }
+
+          if (element->getPartialValue(&buffer[0], offset, nbytes).good())
+          {
+            output.GetLowLevelOutput().Send(&buffer[0], nbytes);
+            offset += nbytes;
+          }
+          else
+          {
+            return;
+          }
+        }
+
+        output.MarkLowLevelOutputDone();
+      }
+    }
+  }
+
+  void ParsedDicomFile::SendPathValue(RestApiOutput& output,
+                                      const UriComponents& uri)
+  {
+    DcmItem* dicom = file_->getDataset();
+
+    // Go down in the tag hierarchy according to the URI
+    for (size_t pos = 0; pos < uri.size() / 2; pos++)
+    {
+      size_t index;
+      try
+      {
+        index = boost::lexical_cast<size_t>(uri[2 * pos + 1]);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return;
+      }
+
+      DcmTagKey k;
+      DcmItem *child = NULL;
+      if (!ParseTagAndGroup(k, uri[2 * pos]) ||
+          !dicom->findAndGetSequenceItem(k, child, index).good() ||
+          child == NULL)
+      {
+        return;
+      }
+
+      dicom = child;
+    }
+
+    // We have reached the end of the URI
+    if (uri.size() % 2 == 0)
+    {
+      SendPathValueForDictionary(output, *dicom);
+    }
+    else
+    {
+      SendPathValueForLeaf(output, uri.back(), *dicom);
+    }
+  }
+
+
   void FromDcmtkBridge::Convert(DicomMap& target, DcmDataset& dataset)
   {
     target.Clear();
--- a/OrthancServer/FromDcmtkBridge.h	Mon Dec 10 17:49:14 2012 +0100
+++ b/OrthancServer/FromDcmtkBridge.h	Thu Dec 13 14:52:57 2012 +0100
@@ -33,8 +33,13 @@
 #pragma once
 
 #include "../Core/DicomFormat/DicomMap.h"
+#include "../Core/RestApi/RestApiOutput.h"
+#include "../Core/Toolbox.h"
+
 #include <dcmtk/dcmdata/dcdatset.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
 #include <json/json.h>
+#include <memory>
 
 namespace Orthanc
 {
@@ -52,6 +57,23 @@
     DicomRootLevel_Instance
   };
 
+  class ParsedDicomFile : public IDynamicObject
+  {
+  private:
+    std::auto_ptr<DcmFileFormat> file_;
+
+  public:
+    ParsedDicomFile(const std::string& content);
+
+    DcmFileFormat& GetDicom()
+    {
+      return *file_;
+    }
+
+    void SendPathValue(RestApiOutput& output,
+                       const UriComponents& uri);
+  };
+
   class FromDcmtkBridge
   {
   public:
--- a/OrthancServer/OrthancRestApi.cpp	Mon Dec 10 17:49:14 2012 +0100
+++ b/OrthancServer/OrthancRestApi.cpp	Thu Dec 13 14:52:57 2012 +0100
@@ -847,6 +847,23 @@
 
 
 
+  // Raw access to the DICOM tags of an instance ------------------------------
+
+  static void GetRawContent(RestApi::GetCall& call)
+  {
+    // TODO IMPROVE MULTITHREADING
+    static boost::mutex mutex_;
+    boost::mutex::scoped_lock lock(mutex_);
+
+    RETRIEVE_CONTEXT(call);
+    std::string id = call.GetUriComponent("id", "");
+    ParsedDicomFile& dicom = context.GetDicomFile(id);
+    dicom.SendPathValue(call.GetOutput(), call.GetTrailingUri());
+  }
+
+
+
+
   // Registration of the various REST handlers --------------------------------
 
   OrthancRestApi::OrthancRestApi(ServerContext& context) : 
@@ -885,6 +902,7 @@
     Register("/instances/{id}/tags", GetInstanceTags<false>);
     Register("/instances/{id}/simplified-tags", GetInstanceTags<true>);
     Register("/instances/{id}/frames", ListFrames);
+    Register("/instances/{id}/content/*", GetRawContent);
 
     Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>);
     Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
--- a/OrthancServer/ServerContext.cpp	Mon Dec 10 17:49:14 2012 +0100
+++ b/OrthancServer/ServerContext.cpp	Thu Dec 13 14:52:57 2012 +0100
@@ -37,6 +37,8 @@
 #include <glog/logging.h>
 
 
+static const size_t DICOM_CACHE_SIZE = 2;
+
 /**
  * IMPORTANT: We make the assumption that the same instance of
  * FileStorage can be accessed from multiple threads. This seems OK
@@ -51,7 +53,9 @@
   ServerContext::ServerContext(const boost::filesystem::path& path) :
     storage_(path.string()),
     index_(*this, path.string()),
-    accessor_(storage_)
+    accessor_(storage_),
+    provider_(*this),
+    dicomCache_(provider_, DICOM_CACHE_SIZE)
   {
     // TODO RECYCLING SETUP HERE
     //index_.SetMaximumPatientCount(4);
@@ -165,4 +169,18 @@
     accessor_.SetCompressionForNextOperations(attachment.GetCompressionType());
     accessor_.Read(result, attachment.GetUuid());
   }
+
+
+  IDynamicObject* ServerContext::DicomCacheProvider::Provide(const std::string& instancePublicId)
+  {
+    std::string content;
+    context_.ReadFile(content, instancePublicId, FileContentType_Dicom);
+    return new ParsedDicomFile(content);
+  }
+
+
+  ParsedDicomFile& ServerContext::GetDicomFile(const std::string& instancePublicId)
+  {
+    return dynamic_cast<ParsedDicomFile&>(dicomCache_.Access(instancePublicId));
+  }
 }
--- a/OrthancServer/ServerContext.h	Mon Dec 10 17:49:14 2012 +0100
+++ b/OrthancServer/ServerContext.h	Thu Dec 13 14:52:57 2012 +0100
@@ -32,10 +32,12 @@
 
 #pragma once
 
-#include "ServerIndex.h"
+#include "../Core/Cache/MemoryCache.h"
 #include "../Core/FileStorage/CompressedFileStorageAccessor.h"
 #include "../Core/FileStorage/FileStorage.h"
 #include "../Core/RestApi/RestApiOutput.h"
+#include "ServerIndex.h"
+#include "FromDcmtkBridge.h"
 
 namespace Orthanc
 {
@@ -47,10 +49,26 @@
   class ServerContext
   {
   private:
+    class DicomCacheProvider : public ICachePageProvider
+    {
+    private:
+      ServerContext& context_;
+
+    public:
+      DicomCacheProvider(ServerContext& context) : context_(context)
+      {
+      }
+      
+      virtual IDynamicObject* Provide(const std::string& id);
+    };
+
     FileStorage storage_;
     ServerIndex index_;
     CompressedFileStorageAccessor accessor_;
     bool compressionEnabled_;
+    
+    DicomCacheProvider provider_;
+    MemoryCache dicomCache_;
 
   public:
     ServerContext(const boost::filesystem::path& path);
@@ -86,5 +104,8 @@
     void ReadFile(std::string& result,
                   const std::string& instancePublicId,
                   FileContentType content);
+
+    // TODO IMPLEMENT MULTITHREADING FOR THIS METHOD
+    ParsedDicomFile& GetDicomFile(const std::string& instancePublicId);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UnitTests/MemoryCache.cpp	Thu Dec 13 14:52:57 2012 +0100
@@ -0,0 +1,128 @@
+#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(CacheIndex, Basic)
+{
+  Orthanc::CacheIndex<std::string> r;
+  
+  r.Add("d");
+  r.Add("a");
+  r.Add("c");
+  r.Add("b");
+
+  r.TagAsMostRecent("a");
+  r.TagAsMostRecent("d");
+  r.TagAsMostRecent("b");
+  r.TagAsMostRecent("c");
+  r.TagAsMostRecent("d");
+  r.TagAsMostRecent("c");
+
+  ASSERT_EQ("a", r.RemoveOldest());
+  ASSERT_EQ("b", r.RemoveOldest());
+  ASSERT_EQ("d", r.RemoveOldest());
+  ASSERT_EQ("c", r.RemoveOldest());
+
+  ASSERT_TRUE(r.IsEmpty());
+}
+
+
+TEST(CacheIndex, Payload)
+{
+  Orthanc::CacheIndex<std::string, int> r;
+  
+  r.Add("a", 420);
+  r.Add("b", 421);
+  r.Add("c", 422);
+  r.Add("d", 423);
+
+  r.TagAsMostRecent("a");
+  r.TagAsMostRecent("d");
+  r.TagAsMostRecent("b");
+  r.TagAsMostRecent("c");
+  r.TagAsMostRecent("d");
+  r.TagAsMostRecent("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.RemoveOldest(p)); ASSERT_EQ(420, p);
+  ASSERT_EQ("d", r.RemoveOldest(p)); ASSERT_EQ(423, p);
+  ASSERT_EQ("c", r.RemoveOldest(p)); ASSERT_EQ(422, p);
+
+  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_);
+}