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
