incubator-deltacloud-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From mfoj...@redhat.com
Subject [PATCH core] New rack middleware for handling Accept headers and returning the correct content type to client
Date Tue, 28 Jun 2011 13:51:42 GMT
From: Michal Fojtik <mfojtik@redhat.com>


Signed-off-by: Michal fojtik <mfojtik@redhat.com>
---
 client/specs/content_spec.rb                    |  152 ++++++++++++++
 server/lib/deltacloud/base_driver/exceptions.rb |    7 +
 server/lib/sinatra/rack_accept.rb               |  152 ++++++++++++++
 server/lib/sinatra/respond_to.rb                |  248 -----------------------
 server/server.rb                                |    7 +-
 5 files changed, 315 insertions(+), 251 deletions(-)
 create mode 100644 client/specs/content_spec.rb
 create mode 100644 server/lib/sinatra/rack_accept.rb
 delete mode 100644 server/lib/sinatra/respond_to.rb

diff --git a/client/specs/content_spec.rb b/client/specs/content_spec.rb
new file mode 100644
index 0000000..78ae937
--- /dev/null
+++ b/client/specs/content_spec.rb
@@ -0,0 +1,152 @@
+#
+# Copyright (C) 2009-2011  Red Hat, Inc.
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+require 'specs/spec_helper'
+
+def client
+  RestClient::Resource.new(API_URL)
+end
+
+def headers(header)
+  encoded_credentials = ["#{API_NAME}:#{API_PASSWORD}"].pack("m0").gsub(/\n/,'')
+  { :authorization => "Basic " + encoded_credentials }.merge(header)
+end
+
+describe "return JSON" do
+
+  it 'should return JSON when using application/json, */*' do
+    header_hash = {
+      'Accept' => "application/json, */*"
+    }
+    client.get(header_hash) do |response, request, &block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^application\/json/
+    end
+  end
+
+  it 'should return JSON when using just application/json' do
+    header_hash = {
+      'Accept' => "application/json"
+    }
+    client.get(header_hash) do |response, request, &block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^application\/json/
+    end
+  end
+
+end
+
+describe "return HTML in different browsers" do
+
+  it "wants XML using format parameter" do
+    client['?format=xml'].get('Accept' => 'application/xhtml+xml') do |response, request,
&block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^application\/xml/
+    end
+  end
+
+  it "wants HTML using format parameter and accept set to XML" do
+    client['?format=html'].get('Accept' => 'application/xml') do |response, request, &block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^text\/html/
+    end
+  end
+
+  it "wants a PNG image" do 
+    client['instance_states?format=png'].get('Accept' => 'image/png') do |response, request,
&block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^image\/png/
+    end
+  end
+
+  it "doesn't have accept header" do
+    client.get('Accept' => '') do |response, request, &block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^application\/xml/
+    end
+  end
+
+  it "can handle unknown formats" do
+    client.get('Accept' => 'format/unknown') do |response, request, &block|
+      response.code.should == 406
+    end
+  end
+
+  it "wants explicitly XML" do
+    client.get('Accept' => 'application/xml') do |response, request, &block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^application\/xml/
+    end
+  end
+
+  it "Internet Explorer" do
+    header_hash = {
+      'Accept' => "text/html, application/xhtml+xml, */*",
+      'User-agent' => "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)"
+    }
+    client.get(header_hash) do |response, request, &block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^text\/html/
+    end
+  end
+
+  it "Mozilla Firefox" do
+    client.get('Accept' => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
do |response, request, &block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^text\/html/
+    end
+  end
+
+  it "Chrome" do
+    header_hash = { 
+      'Accept' => "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
+      'User-agent' => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1
(KHTML, like Gecko) Chrome/14.0.790.0 Safari/535.1"
+    }
+    client.get(header_hash) do |response, request, &block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^text\/html/
+    end
+  end
+
+  it "Safari" do
+    header_hash = { 
+      'Accept' => "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
+      'User-agent' => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; da-dk) AppleWebKit/533.21.1
(KHTML, like Gecko) Version/5.0.5 Safari/533.21.1"
+    }
+    client.get(header_hash) do |response, request, &block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^text\/html/
+    end
+  end
+
+  it "Opera" do
+    header_hash = { 
+      'Accept' => "text/html, application/xml;q=0.9, application/xhtml+xml, image/png,
image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1",
+      'User-agent' => "Opera/9.80 (X11; Linux i686; U; ru) Presto/2.8.131 Version/11.11"
+    }
+    client.get(header_hash) do |response, request, &block|
+      response.code.should == 200
+      response.headers[:content_type].should =~ /^text\/html/
+    end
+  end
+
+
+
+  
+
+end
diff --git a/server/lib/deltacloud/base_driver/exceptions.rb b/server/lib/deltacloud/base_driver/exceptions.rb
index 391910f..d95191c 100644
--- a/server/lib/deltacloud/base_driver/exceptions.rb
+++ b/server/lib/deltacloud/base_driver/exceptions.rb
@@ -21,6 +21,13 @@ module Deltacloud
       end
     end
 
+    class UnknownMediaTypeError < DeltacloudException
+      def initialize(e, message=nil)
+        message ||= e.message
+        super(406, e.class.name, message, e.backtrace)
+      end
+    end
+
     class ValidationFailure < DeltacloudException
       def initialize(e, message=nil)
         message ||= e.message
diff --git a/server/lib/sinatra/rack_accept.rb b/server/lib/sinatra/rack_accept.rb
new file mode 100644
index 0000000..9dddadd
--- /dev/null
+++ b/server/lib/sinatra/rack_accept.rb
@@ -0,0 +1,152 @@
+# respond_to (The MIT License)
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+# and associated documentation files (the 'Software'), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge, publish, distribute,
+# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
is
+# furnished to do so, subject to the following conditions:
+#
+# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT
+# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+require 'sinatra/base'
+require 'rack/accept'
+
+use Rack::Accept
+
+module Rack
+
+  module RespondTo
+    
+    # This method is triggered after this helper is registred
+    # within Sinatra.
+    # We need to overide the default render method to supply correct path to the
+    # template, since Sinatra is by default looking in the current __FILE__ path
+    def self.registered(app)
+      app.helpers Rack::RespondTo::Helpers
+      app.class_eval do
+        alias :render_without_format :render
+        def render(*args, &block)
+          begin
+            assumed_layout = args[1] == :layout
+            args[1] = "#{args[1]}.#{format}".to_sym if args[1].is_a?(::Symbol)
+            render_without_format *args, &block
+          rescue Errno::ENOENT => e
+            raise "ERROR: Missing template: #{args[1]}.#{args[0]}" unless assumed_layout
+            raise e
+          end
+        end
+        private :render
+      end
+    end
+
+      module Helpers
+
+        # This code was inherited from respond_to plugin
+        # http://github.com/cehoffman/sinatra-respond_to
+        #
+        # This method is used to overide the default content_type returned from
+        # rack-accept middleware.
+        def self.included(klass)
+          klass.class_eval do
+            alias :content_type_without_save :content_type
+            def content_type(*args)
+              content_type_without_save *args
+              request.env['rack-accept.format'] = args.first.to_sym
+              response['Content-Type']
+            end
+          end
+        end
+
+        def format(val=nil)
+          request.env['rack-accept.format'] ||= val
+          request.env['rack-accept.format'].to_sym
+        end
+
+        def static_file?(path)
+          public_dir = File.expand_path(options.public)
+          path = File.expand_path(File.join(public_dir, unescape(path)))
+          path[0, public_dir.length] == public_dir && File.file?(path)
+        end
+
+        def respond_to(&block)
+          wants = {}
+          def wants.method_missing(type, *args, &handler)
+            self[type] = handler
+          end
+          yield wants
+          raise Deltacloud::ExceptionHandler::UnknownMediaTypeError::new(nil, "Unknown format")
unless wants[format]
+          wants[format].call
+        end
+
+    end
+
+  end
+
+  class MediaType < Sinatra::Base
+
+    include Rack::RespondTo::Helpers
+
+    # Define supported media types here
+    # The :return key stands for content-type which will be returned
+    # The :match key stands for the matching Accept header 
+    ACCEPTED_MEDIA_TYPES = {
+      :xml => { :return => 'application/xml', :match => ['application/xml', 'text/xml']
},
+      :json => { :return => 'application/json', :match => ['application/json'] },
+      :html => { :return => 'text/html', :match => ['application/xhtml+xml', 'text/html']
},
+      :png => { :return => 'image/png', :match => ['image/png'] },
+      :gv => { :return => 'application/ghostscript', :match => ['application/ghostscript']
}
+    }
+
+    def call(env)
+      accept, index = env['rack-accept.request'], {}
+
+      # Skip everything when 'format' parameter is set in URL
+      if env['rack.request.query_hash']["format"]
+         media_type = case env['rack.request.query_hash']["format"]
+            when 'html' then :html 
+            when 'xml' then :xml
+            when 'json' then :json
+            when 'gv' then :gv
+            when 'png' then :png
+          end
+        index[media_type] = 1 if media_type
+      else
+        # Sort all requested media types in Accept using their 'q' values
+        sorted_media_types = accept.media_type.qvalues.to_a.sort{ |a,b| b[1]<=>a[1]
}.collect { |t| t.first }
+        # If Accept header is missing or is empty, fallback to XML format
+        sorted_media_types << 'application/xml' if sorted_media_types.empty?
+        # Choose the right format with the media type according to the priority
+        ACCEPTED_MEDIA_TYPES.each do |format, definition|
+          definition[:match].each do |media_type|
+            break if index[format] = sorted_media_types.index(media_type)
+          end
+        end
+        # Reject formats with no/nil priority
+        index.reject! { |format, priority| not priority }
+      end
+      
+      # If after all we don't have any matching format assume that client has
+      # requested unknown/wrong media type and throw an 406 error with no body
+      if index.keys.empty?
+        status, headers, response = 406, {}, ""
+      else
+        media_type = index.to_a.sort{ |a, b| a[1]<=>b[1] }.first[0]
+        # Set this environment variable for futher pickup by the 'format' helper
+        # on top
+        env['rack-accept.format'] = media_type
+        status, headers, response = @app.call(env)
+        # Overide the Content-type with :return value of matching format
+        headers['Content-Type'] = ACCEPTED_MEDIA_TYPES[media_type][:return]
+      end
+
+      [status, headers, response]
+    end
+
+  end
+
+end
+
diff --git a/server/lib/sinatra/respond_to.rb b/server/lib/sinatra/respond_to.rb
deleted file mode 100644
index 139573b..0000000
--- a/server/lib/sinatra/respond_to.rb
+++ /dev/null
@@ -1,248 +0,0 @@
-# respond_to (The MIT License)
-
-# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
-# and associated documentation files (the 'Software'), to deal in the Software without restriction,
-# including without limitation the rights to use, copy, modify, merge, publish, distribute,
-# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
is
-# furnished to do so, subject to the following conditions:
-#
-# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT
-# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
-
-require 'sinatra/base'
-require 'rack/accept'
-
-use Rack::Accept
-
-module Sinatra
-  module RespondTo
-
-    class MissingTemplate < Sinatra::NotFound; end
-
-    # Define all MIME types you want to support here.
-    # This conversion table will be used for auto-negotiation
-    # with browser in sinatra when no 'format' parameter is specified.
-
-    SUPPORTED_ACCEPT_HEADERS = {
-      :xml => [
-        'text/xml',
-        'application/xml'
-      ],
-      :html => [
-        'text/html',
-        'application/xhtml+xml'
-      ],
-      :json => [
-        'application/json'
-      ]
-    }
-
-    # We need to pass array of available response types to
-    # best_media_type method
-    def accept_to_array
-      SUPPORTED_ACCEPT_HEADERS.keys.collect do |key|
-        SUPPORTED_ACCEPT_HEADERS[key]
-      end.flatten
-    end
-
-    # Then, when we get best media type for response, we need
-    # to know which format to choose
-    def lookup_format_from_mime(mime)
-      SUPPORTED_ACCEPT_HEADERS.keys.each do |format|
-        return format if SUPPORTED_ACCEPT_HEADERS[format].include?(mime)
-      end
-    end
-
-    def self.registered(app)
-
-      app.helpers RespondTo::Helpers
-
-      app.before do
-
-        # Skip development error image and static content
-        next if self.class.development? && request.path_info =~ %r{/__sinatra__/.*?.png}
-        next if options.static? && options.public? && (request.get? || request.head?)
&& static_file?(request.path_info)
-
-        # Remove extension from URI
-        # Extension will be available as a 'extension' method (extension=='txt')
-
-        extension request.path_info.match(/\.([^\.\/]+)$/).to_a.first
-
-        # If ?format= is present, ignore all Accept negotiations because
-        # we are not dealing with browser
-        if request.params.has_key? 'format'
-          format params['format'].to_sym
-        end
-
-        # Let's make a little exception here to handle
-        # /api/instance_states[.gv/.png] calls
-        if extension.eql?('gv')
-          format :gv
-        elsif extension.eql?('png')
-          format :png
-        end
-
-        # Get Rack::Accept::Response object and find best possible
-        # mime type to output.
-        # This negotiation works fine with latest rest-client gem:
-        #
-        # RestClient.get 'http://localhost:3001/api', {:accept => :json } =>
-        # 'application/json'
-        # RestClient.get 'http://localhost:3001/api', {:accept => :xml } =>
-        # 'application/xml'
-        #
-        # Also browsers like Firefox (3.6.x) and Chromium reporting
-        # 'application/xml+xhtml' which is recognized as :html reponse
-        # In browser you can force output using ?format=[format] parameter.
-
-        rack_accept = env['rack-accept.request']
-
-        if rack_accept.media_type.to_s.strip.eql?('Accept:')
-          format :xml
-        elsif is_chrome?
-          format :html
-        else
-          format lookup_format_from_mime(rack_accept.best_media_type(accept_to_array))
-        end
-
-      end
-
-      app.class_eval do
-
-        # Simple helper to detect Chrome based browsers
-        # which have screwed up they Accept headers.
-        # Set HTML as default output format here
-        def is_chrome?
-          true if env['HTTP_USER_AGENT'] =~ /Chrome/
-        end
-
-        # This code was copied from respond_to plugin
-        # http://github.com/cehoffman/sinatra-respond_to
-        # MIT License
-        alias :render_without_format :render
-        def render(*args, &block)
-          assumed_layout = args[1] == :layout
-          args[1] = "#{args[1]}.#{format}".to_sym if args[1].is_a?(::Symbol)
-          render_without_format *args, &block
-        rescue Errno::ENOENT => e
-          raise MissingTemplate, "#{args[1]}.#{args[0]}" unless assumed_layout
-          raise e
-        end
-        private :render
-      end
-
-      # This code was copied from respond_to plugin
-      # http://github.com/cehoffman/sinatra-respond_to
-      app.configure :development do |dev|
-        dev.error MissingTemplate do
-          content_type :html, :charset => 'utf-8'
-          response.status = request.env['sinatra.error'].code
-
-          engine = request.env['sinatra.error'].message.split('.').last
-          engine = 'haml' unless ['haml', 'builder', 'erb'].include? engine
-
-          path = File.basename(request.path_info)
-          path = "root" if path.nil? || path.empty?
-
-          format = engine == 'builder' ? 'xml' : 'html'
-
-          layout = case engine
-                   when 'haml' then "!!!\n%html\n  %body= yield"
-                   when 'erb' then "<html>\n  <body>\n    <%= yield %>\n
 </body>\n</html>"
-                   end
-
-          layout = "<small>app.#{format}.#{engine}</small>\n<pre>#{escape_html(layout)}</pre>"
-
-          (<<-HTML).gsub(/^ {10}/, '')
-          <!DOCTYPE html>
-          <html>
-          <head>
-            <style type="text/css">
-            body { text-align:center;font-family:helvetica,arial;font-size:22px;
-              color:#888;margin:20px}
-            #c {margin:0 auto;width:500px;text-align:left;}
-            small {float:right;clear:both;}
-            pre {clear:both;text-align:left;font-size:70%;width:500px;margin:0 auto;}
-            </style>
-          </head>
-          <body>
-            <h2>Sinatra can't find #{request.env['sinatra.error'].message}</h2>
-            <img src='/__sinatra__/500.png'>
-            <pre>#{request.env['sinatra.error'].backtrace.join("\n")}</pre>
-            <div id="c">
-              <small>application.rb</small>
-              <pre>#{request.request_method.downcase} '#{request.path_info}' do\n 
respond_to do |wants|\n    wants.#{format} { #{engine} :#{path} }\n  end\nend</pre>
-            </div>
-          </body>
-          </html>
-          HTML
-        end
-
-      end
-    end
-
-    module Helpers
-
-      # This code was copied from respond_to plugin
-      # http://github.com/cehoffman/sinatra-respond_to
-      def self.included(klass)
-        klass.class_eval do
-          alias :content_type_without_save :content_type
-          def content_type(*args)
-            content_type_without_save *args
-            @_format = args.first.to_sym
-            response['Content-Type']
-          end
-        end
-      end
-
-      def static_file?(path)
-        public_dir = File.expand_path(options.public)
-        path = File.expand_path(File.join(public_dir, unescape(path)))
-
-        path[0, public_dir.length] == public_dir && File.file?(path)
-      end
-
-
-      # Extension holds trimmed extension. This is extra usefull
-      # when you want to build original URI (with extension)
-      # You can simply call "#{request.env['REQUEST_URI']}.#{extension}"
-      def extension(val=nil)
-        @_extension ||= val
-        @_extension
-      end
-
-      # This helper will holds current format. Helper should be
-      # accesible from all places in Sinatra
-      def format(val=nil)
-        @_format ||= val
-        @_format
-      end
-
-      def respond_to(&block)
-        wants = {}
-
-        def wants.method_missing(type, *args, &handler)
-          self[type] = handler
-        end
-
-        # Set proper content-type and encoding for
-        # text based formats
-        if [:xml, :gv, :html, :json].include?(format)
-          content_type format, :charset => 'utf-8'
-        end
-        yield wants
-        # Raise this error if requested format is not defined
-        # in respond_to { } block.
-        raise MissingTemplate if wants[format].nil?
-
-        wants[format].call
-      end
-
-    end
-
-  end
-end
diff --git a/server/server.rb b/server/server.rb
index 104ea9c..787d8e4 100644
--- a/server/server.rb
+++ b/server/server.rb
@@ -17,7 +17,7 @@ require 'sinatra'
 require 'deltacloud'
 require 'drivers'
 require 'json'
-require 'sinatra/respond_to'
+require 'sinatra/rack_accept'
 require 'sinatra/static_assets'
 require 'sinatra/rabbit'
 require 'sinatra/lazy_auth'
@@ -35,10 +35,13 @@ set :version, '0.3.0'
 include Deltacloud::Drivers
 set :drivers, Proc.new { driver_config }
 
+Sinatra::Application.register Rack::RespondTo
+
 use Rack::ETag
 use Rack::Runtime
 use Rack::MatrixParams
 use Rack::DriverSelect
+use Rack::MediaType
 
 configure do
   set :raise_errors => false
@@ -63,8 +66,6 @@ error do
   report_error
 end
 
-Sinatra::Application.register Sinatra::RespondTo
-
 # Redirect to /api
 get '/' do redirect root_url, 301; end
 
-- 
1.7.4.1


Mime
View raw message