changeset 1781:5ad4e4d92ecb

AcceptMediaDispatcher bootstrap
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 17 Nov 2015 17:46:32 +0100
parents 94990da8710e
children 9f2df9bb2cdf
files Core/Enumerations.cpp Core/Enumerations.h OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/ParsedDicomFile.cpp OrthancServer/ParsedDicomFile.h OrthancServer/main.cpp Plugins/Include/orthanc/OrthancCPlugin.h Resources/ErrorCodes.json UnitTestsSources/RestApiTests.cpp
diffstat 9 files changed, 485 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- a/Core/Enumerations.cpp	Fri Nov 13 15:06:45 2015 +0100
+++ b/Core/Enumerations.cpp	Tue Nov 17 17:46:32 2015 +0100
@@ -151,6 +151,9 @@
       case ErrorCode_EmptyRequest:
         return "The request is empty";
 
+      case ErrorCode_NotAcceptable:
+        return "Cannot send a response which is acceptable according to the Accept HTTP header";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
@@ -1131,6 +1134,9 @@
       case ErrorCode_Unauthorized:
         return HttpStatus_401_Unauthorized;
 
+      case ErrorCode_NotAcceptable:
+        return HttpStatus_406_NotAcceptable;
+
       default:
         return HttpStatus_500_InternalServerError;
     }
--- a/Core/Enumerations.h	Fri Nov 13 15:06:45 2015 +0100
+++ b/Core/Enumerations.h	Tue Nov 17 17:46:32 2015 +0100
@@ -80,6 +80,7 @@
     ErrorCode_DatabasePlugin = 31    /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */,
     ErrorCode_StorageAreaPlugin = 32    /*!< Error in the plugin implementing a custom storage area */,
     ErrorCode_EmptyRequest = 33    /*!< The request is empty */,
+    ErrorCode_NotAcceptable = 34    /*!< Cannot send a response which is acceptable according to the Accept HTTP header */,
     ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Fri Nov 13 15:06:45 2015 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Tue Nov 17 17:46:32 2015 +0100
@@ -259,6 +259,32 @@
   }
 
 
+  static void AnswerImage(RestApiGetCall& call,
+                          ParsedDicomFile& dicom,
+                          unsigned int frame,
+                          ImageExtractionMode mode)
+  {
+    typedef std::vector<std::string>  MediaRanges;
+
+    // Get the HTTP "Accept" header, if any
+    std::string accept = call.GetHttpHeader("accept", "*/*");
+
+    MediaRanges mediaRanges;
+    Toolbox::TokenizeString(mediaRanges, accept, ',');
+
+    for (MediaRanges::const_reverse_iterator it = mediaRanges.rbegin();
+         it != mediaRanges.rend(); ++it)
+    {
+    }
+
+    throw OrthancException(ErrorCode_NotAcceptable);
+
+    std::string image;
+    dicom.ExtractPngImage(image, frame, mode);
+    call.GetOutput().AnswerBuffer(image, "image/png");
+  }
+
+
   template <enum ImageExtractionMode mode>
   static void GetImage(RestApiGetCall& call)
   {
@@ -277,15 +303,14 @@
     }
 
     std::string publicId = call.GetUriComponent("id", "");
-    std::string dicomContent, png;
+    std::string dicomContent;
     context.ReadFile(dicomContent, publicId, FileContentType_Dicom);
 
     ParsedDicomFile dicom(dicomContent);
 
     try
     {
-      dicom.ExtractPngImage(png, frame, mode);
-      call.GetOutput().AnswerBuffer(png, "image/png");
+      AnswerImage(call, dicom, frame, mode);
     }
     catch (OrthancException& e)
     {
--- a/OrthancServer/ParsedDicomFile.cpp	Fri Nov 13 15:06:45 2015 +0100
+++ b/OrthancServer/ParsedDicomFile.cpp	Tue Nov 17 17:46:32 2015 +0100
@@ -84,14 +84,16 @@
 #include "FromDcmtkBridge.h"
 #include "ToDcmtkBridge.h"
 #include "Internals/DicomImageDecoder.h"
-#include "../Core/Logging.h"
-#include "../Core/Toolbox.h"
-#include "../Core/OrthancException.h"
+#include "../Core/DicomFormat/DicomIntegerPixelAccessor.h"
 #include "../Core/Images/ImageBuffer.h"
+#include "../Core/Images/JpegWriter.h"
+#include "../Core/Images/JpegReader.h"
+#include "../Core/Images/PngReader.h"
 #include "../Core/Images/PngWriter.h"
+#include "../Core/Logging.h"
+#include "../Core/OrthancException.h"
+#include "../Core/Toolbox.h"
 #include "../Core/Uuid.h"
-#include "../Core/DicomFormat/DicomIntegerPixelAccessor.h"
-#include "../Core/Images/PngReader.h"
 
 #include <list>
 #include <limits>
@@ -796,36 +798,6 @@
   }
 
 
-  template <typename T>
-  static void ExtractPngImageTruncate(std::string& result,
-                                      DicomIntegerPixelAccessor& accessor,
-                                      PixelFormat format)
-  {
-    assert(accessor.GetInformation().GetChannelCount() == 1);
-
-    PngWriter w;
-
-    std::vector<T> image(accessor.GetInformation().GetWidth() * accessor.GetInformation().GetHeight(), 0);
-    T* pixel = &image[0];
-    for (unsigned int y = 0; y < accessor.GetInformation().GetHeight(); y++)
-    {
-      for (unsigned int x = 0; x < accessor.GetInformation().GetWidth(); x++, pixel++)
-      {
-        int32_t v = accessor.GetValue(x, y);
-        if (v < static_cast<int32_t>(std::numeric_limits<T>::min()))
-          *pixel = std::numeric_limits<T>::min();
-        else if (v > static_cast<int32_t>(std::numeric_limits<T>::max()))
-          *pixel = std::numeric_limits<T>::max();
-        else
-          *pixel = static_cast<T>(v);
-      }
-    }
-
-    w.WriteToMemory(result, accessor.GetInformation().GetWidth(), accessor.GetInformation().GetHeight(),
-                    accessor.GetInformation().GetWidth() * sizeof(T), format, &image[0]);
-  }
-
-
   void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer)
   {
     FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset());
@@ -903,7 +875,8 @@
     Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme);
     Toolbox::ToLowerCase(mime);
 
-    if (mime == "image/png")
+    if (mime == "image/png" ||
+        mime == "image/jpeg")
     {
       EmbedImage(mime, content);
     }
@@ -928,6 +901,12 @@
       reader.ReadFromMemory(content);
       EmbedImage(reader);
     }
+    else if (mime == "image/jpeg")
+    {
+      JpegReader reader;
+      reader.ReadFromMemory(content);
+      EmbedImage(reader);
+    }
     else
     {
       throw OrthancException(ErrorCode_NotImplemented);
@@ -1100,6 +1079,27 @@
   }
 
 
+  void ParsedDicomFile::ExtractJpegImage(std::string& result,
+                                         unsigned int frame,
+                                         ImageExtractionMode mode,
+                                         uint8_t quality)
+  {
+    if (mode != ImageExtractionMode_UInt8 &&
+        mode != ImageExtractionMode_Preview)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    ImageBuffer buffer;
+    ExtractImage(buffer, frame, mode);
+
+    ImageAccessor accessor(buffer.GetConstAccessor());
+    JpegWriter writer;
+    writer.SetQuality(quality);
+    writer.WriteToMemory(result, accessor);
+  }
+
+
   Encoding ParsedDicomFile::GetEncoding() const
   {
     return FromDcmtkBridge::DetectEncoding(*pimpl_->file_->getDataset());
--- a/OrthancServer/ParsedDicomFile.h	Fri Nov 13 15:06:45 2015 +0100
+++ b/OrthancServer/ParsedDicomFile.h	Tue Nov 17 17:46:32 2015 +0100
@@ -129,6 +129,11 @@
                          unsigned int frame,
                          ImageExtractionMode mode);
 
+    void ExtractJpegImage(std::string& result,
+                          unsigned int frame,
+                          ImageExtractionMode mode,
+                          uint8_t quality);
+
     Encoding GetEncoding() const;
 
     void SetEncoding(Encoding encoding);
--- a/OrthancServer/main.cpp	Fri Nov 13 15:06:45 2015 +0100
+++ b/OrthancServer/main.cpp	Tue Nov 17 17:46:32 2015 +0100
@@ -492,6 +492,7 @@
     PrintErrorCode(ErrorCode_DatabasePlugin, "The plugin implementing a custom database back-end does not fulfill the proper interface");
     PrintErrorCode(ErrorCode_StorageAreaPlugin, "Error in the plugin implementing a custom storage area");
     PrintErrorCode(ErrorCode_EmptyRequest, "The request is empty");
+    PrintErrorCode(ErrorCode_NotAcceptable, "Cannot send a response which is acceptable according to the Accept HTTP header");
     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Nov 13 15:06:45 2015 +0100
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Nov 17 17:46:32 2015 +0100
@@ -213,6 +213,7 @@
     OrthancPluginErrorCode_DatabasePlugin = 31    /*!< The plugin implementing a custom database back-end does not fulfill the proper interface */,
     OrthancPluginErrorCode_StorageAreaPlugin = 32    /*!< Error in the plugin implementing a custom storage area */,
     OrthancPluginErrorCode_EmptyRequest = 33    /*!< The request is empty */,
+    OrthancPluginErrorCode_NotAcceptable = 34    /*!< Cannot send a response which is acceptable according to the Accept HTTP header */,
     OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
--- a/Resources/ErrorCodes.json	Fri Nov 13 15:06:45 2015 +0100
+++ b/Resources/ErrorCodes.json	Tue Nov 17 17:46:32 2015 +0100
@@ -189,6 +189,12 @@
     "Code": 33,
     "Name": "EmptyRequest",
     "Description": "The request is empty"
+  }, 
+  {
+    "Code": 34, 
+    "HttpStatus": 406, 
+    "Name": "NotAcceptable", 
+    "Description": "Cannot send a response which is acceptable according to the Accept HTTP header"
   },
 
 
--- a/UnitTestsSources/RestApiTests.cpp	Fri Nov 13 15:06:45 2015 +0100
+++ b/UnitTestsSources/RestApiTests.cpp	Tue Nov 17 17:46:32 2015 +0100
@@ -34,6 +34,8 @@
 #include "gtest/gtest.h"
 
 #include <ctype.h>
+#include <boost/lexical_cast.hpp>
+#include <algorithm>
 
 #include "../Core/ChunkedBuffer.h"
 #include "../Core/HttpClient.h"
@@ -335,3 +337,402 @@
   ASSERT_TRUE(HandleGet(root, "/hello2/a/b"));
   ASSERT_EQ(testValue, 4);
 }
+
+
+
+
+namespace Orthanc
+{
+  class AcceptMediaDispatcher : public boost::noncopyable
+  {
+  public:
+    typedef std::map<std::string, std::string>  HttpHeaders;
+
+    class IHandler : public boost::noncopyable
+    {
+    public:
+      virtual ~IHandler()
+      {
+      }
+
+      virtual void Handle(const std::string& type,
+                          const std::string& subtype,
+                          float quality /* between 0 and 1 */) = 0;
+    };
+
+  private:
+    struct Handler
+    {
+      std::string  type_;
+      std::string  subtype_;
+      IHandler&    handler_;
+
+      Handler(const std::string& type,
+              const std::string& subtype,
+              IHandler& handler) :
+        type_(type),
+        subtype_(subtype),
+        handler_(handler)
+      {
+      }
+
+      bool IsMatch(const std::string& type,
+                   const std::string& subtype) const
+      {
+        if (type == "*" && subtype == "*")
+        {
+          return true;
+        }
+        
+        if (subtype == "*" && type == type_)
+        {
+          return true;
+        }
+
+        return type == type_ && subtype == subtype_;
+      }
+    };
+
+
+    struct Reference : public boost::noncopyable
+    {
+      const Handler&  handler_;
+      uint8_t         level_;
+      float           quality_;
+      size_t          specificity_;   // Number of arguments
+
+      Reference(const Handler& handler,
+                const std::string& type,
+                const std::string& subtype,
+                float quality,
+                size_t specificity) :
+        handler_(handler),
+        quality_(quality),
+        specificity_(specificity)
+      {
+        if (type == "*" && subtype == "*")
+        {
+          level_ = 0;
+        }
+        else if (subtype == "*")
+        {
+          level_ = 1;
+        }
+        else
+        {
+          level_ = 2;
+        }
+      }
+      
+      bool operator< (const Reference& other) const
+      {
+        if (level_ < other.level_)
+        {
+          return true;
+        }
+
+        if (level_ > other.level_)
+        {
+          return false;
+        }
+
+        return specificity_ < other.specificity_;
+      }
+
+      void Call() const
+      {
+        handler_.handler_.Handle(handler_.type_, handler_.subtype_, quality_);
+      }
+    };
+
+
+    typedef std::vector<std::string>  Tokens;
+    typedef std::list<Handler>   Handlers;
+
+    Handlers  handlers_;
+
+
+    static bool SplitPair(std::string& first /* out */,
+                          std::string& second /* out */,
+                          const std::string& source,
+                          char separator)
+    {
+      size_t pos = source.find(separator);
+
+      if (pos == std::string::npos)
+      {
+        return false;
+      }
+      else
+      {
+        first = Toolbox::StripSpaces(source.substr(0, pos));
+        second = Toolbox::StripSpaces(source.substr(pos + 1));
+        return true;      
+      }
+    }
+
+
+    static void GetQualityAndSpecificity(float& quality /* out */,
+                                         size_t& specificity /* out */,
+                                         const Tokens& parameters)
+    {
+      assert(!parameters.empty());
+
+      quality = 1.0f;
+      specificity = parameters.size() - 1;
+
+      for (size_t i = 1; i < parameters.size(); i++)
+      {
+        std::string key, value;
+        if (SplitPair(key, value, parameters[i], '=') &&
+            key == "q")
+        {
+          bool ok = false;
+
+          try
+          {
+            quality = boost::lexical_cast<float>(value);
+            ok = (quality >= 0.0f && quality <= 1.0f);
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+          }
+
+          if (ok)
+          {
+            assert(parameters.size() >= 2);
+            specificity = parameters.size() - 2;
+            return;
+          }
+          else
+          {
+            LOG(ERROR) << "Quality parameter out of range in a HTTP request (must be between 0 and 1): " << value;
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+          }
+        }
+      }
+    }
+
+
+    static void SelectBestMatch(std::auto_ptr<Reference>& best,
+                                const Handler& handler,
+                                const std::string& type,
+                                const std::string& subtype,
+                                float quality,
+                                size_t specificity)
+    {
+      std::auto_ptr<Reference> match(new Reference(handler, type, subtype, quality, specificity));
+
+      if (best.get() == NULL ||
+          *best < *match)
+      {
+        best = match;
+      }
+    }
+
+
+  public:
+    void Register(const std::string& mime,
+                  IHandler& handler)
+    {
+      std::string type, subtype;
+
+      if (SplitPair(type, subtype, mime, '/') &&
+          type != "*" &&
+          subtype != "*")
+      {
+        handlers_.push_back(Handler(type, subtype, handler));
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    
+    bool Apply(const HttpHeaders& headers)
+    {
+      HttpHeaders::const_iterator accept = headers.find("accept");
+      if (accept != headers.end())
+      {
+        return Apply(accept->second);
+      }
+      else
+      {
+        return Apply("*/*");
+      }
+    }
+
+
+    bool Apply(const std::string& accept)
+    {
+      // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
+
+      Tokens mediaRanges;
+      Toolbox::TokenizeString(mediaRanges, accept, ',');
+
+      std::auto_ptr<Reference> bestMatch;
+
+      for (Tokens::const_reverse_iterator it = mediaRanges.rbegin();
+           it != mediaRanges.rend(); ++it)
+      {
+        Tokens parameters;
+        Toolbox::TokenizeString(parameters, *it, ';');
+
+        if (parameters.size() > 0)
+        {
+          float quality;
+          size_t specificity;
+          GetQualityAndSpecificity(quality, specificity, parameters);
+
+          std::string type, subtype;
+          if (SplitPair(type, subtype, parameters[0], '/'))
+          {
+            for (Handlers::const_iterator it2 = handlers_.begin();
+                 it2 != handlers_.end(); ++it2)
+            {
+              if (it2->IsMatch(type, subtype))
+              {
+                SelectBestMatch(bestMatch, *it2, type, subtype, quality, specificity);
+              }
+            }
+          }
+        }
+      }
+
+      if (bestMatch.get() == NULL)  // No match was found
+      {
+        return false;
+      }
+      else
+      {
+        bestMatch->Call();
+        return true;
+      }
+    }
+  };
+}
+
+
+
+namespace
+{
+  class AcceptHandler : public Orthanc::AcceptMediaDispatcher::IHandler
+  {
+  private:
+    std::string type_;
+    std::string subtype_;
+    float quality_;
+
+  public:
+    AcceptHandler()
+    {
+      Reset();
+    }
+
+    void Reset()
+    {
+      Handle("nope", "nope", 0.0f);
+    }
+
+    const std::string& GetType() const
+    {
+      return type_;
+    }
+
+    const std::string& GetSubType() const
+    {
+      return subtype_;
+    }
+
+    float GetQuality() const
+    {
+      return quality_;
+    }
+
+    virtual void Handle(const std::string& type,
+                        const std::string& subtype,
+                        float quality /* between 0 and 1 */)
+    {
+      type_ = type;
+      subtype_ = subtype;
+      quality_ = quality;
+    }
+  };
+}
+
+
+TEST(RestApi, AcceptMediaDispatcher)
+{
+  // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
+
+  AcceptHandler h;
+
+  {
+    Orthanc::AcceptMediaDispatcher d;
+    d.Register("audio/mp3", h);
+    d.Register("audio/basic", h);
+
+    ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/basic"));
+    ASSERT_EQ("audio", h.GetType());
+    ASSERT_EQ("basic", h.GetSubType());
+    ASSERT_FLOAT_EQ(1.0f, h.GetQuality());
+
+    ASSERT_TRUE(d.Apply("audio/*; q=0.2, audio/nope"));
+    ASSERT_EQ("audio", h.GetType());
+    ASSERT_EQ("mp3", h.GetSubType());
+    ASSERT_FLOAT_EQ(0.2f, h.GetQuality());
+    
+    ASSERT_FALSE(d.Apply("application/*; q=0.2, application/pdf"));
+    
+    ASSERT_TRUE(d.Apply("*/*; application/*; q=0.2, application/pdf"));
+    ASSERT_EQ("audio", h.GetType());
+  }
+
+  // "This would be interpreted as "text/html and text/x-c are the
+  // preferred media types, but if they do not exist, then send the
+  // text/x-dvi entity, and if that does not exist, send the
+  // text/plain entity.""
+  const std::string T1 = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c";
+  
+  {
+    Orthanc::AcceptMediaDispatcher d;
+    d.Register("text/plain", h);
+    //d.Register("text/x-dvi", h);
+    d.Register("text/html", h);
+    ASSERT_TRUE(d.Apply(T1));
+    ASSERT_EQ("text", h.GetType());
+    ASSERT_EQ("html", h.GetSubType());
+    ASSERT_EQ(1.0f, h.GetQuality());
+  }
+  
+  {
+    Orthanc::AcceptMediaDispatcher d;
+    d.Register("text/plain", h);
+    //d.Register("text/x-dvi", h);
+    d.Register("text/x-c", h);
+    ASSERT_TRUE(d.Apply(T1));
+    ASSERT_EQ("text", h.GetType());
+    ASSERT_EQ("x-c", h.GetSubType());
+    ASSERT_EQ(1.0f, h.GetQuality());
+  }
+  
+  {
+    Orthanc::AcceptMediaDispatcher d;
+    d.Register("text/plain", h);
+    d.Register("text/x-dvi", h);
+    ASSERT_TRUE(d.Apply(T1));
+    ASSERT_EQ("text", h.GetType());
+    ASSERT_EQ("x-dvi", h.GetSubType());
+    ASSERT_EQ(0.8f, h.GetQuality());
+  }
+  
+  {
+    Orthanc::AcceptMediaDispatcher d;
+    d.Register("text/plain", h);
+    ASSERT_TRUE(d.Apply(T1));
+    ASSERT_EQ("text", h.GetType());
+    ASSERT_EQ("plain", h.GetSubType());
+    ASSERT_EQ(0.5f, h.GetQuality());
+  }
+}