From: Michal Fojtik<[email protected]>
Signed-off-by: Michal fojtik<[email protected]>
---
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