Strelka::HTTPResponse::

Negotiation

module
Included Modules
Strelka::Constants
Extended With
Strelka::MethodUtilities

The mixin that adds methods to Strelka::HTTPResponse for content-negotiation.

response = request.response
response.for( 'text/html' ) {...}
response.for( :json ) {...}
response.for_encoding( :en ) {...}
response.for_language( :en ) {...}

If the response was created from a request, it also knows whether or not it is acceptable according to its request's `Accept*` headers.

Constants

BUILTIN_MIMETYPE_MAP

TODO: Perhaps replace this with something like this:

Mongrel2::Config::Mimetype.to_hash( :extension => :mimetype )
BUILTIN_STRINGIFIERS

A collection of default stringifier callbacks, keyed by mimetype. If an entry for the content-negotiation callback's mimetype exists in this Hash, it will be call()ed on the callback's return value to stringify the body.

UNICODE_CHARSETS

Transcoding to Unicode is likely enough to work to warrant auto-transcoding. These are the charsets that will be used for auto-transcoding in the case where the whole entity body isn't in memory

Attributes

encoding_callbacks[R]

The Hash of document coding alternative callbacks for content negotiation, keyed by coding name.

language_callbacks[R]

The Hash of language alternative callbacks for content negotiation, keyed by language tag String.

mediatype_callbacks[R]

The Hash of mediatype alternative callbacks for content negotiation, keyed by mimetype.

vary_fields[R]

A Set of header fields to add to the 'Vary:' response header.

Public Class Methods

anchor
new( * )

Add some instance variables for negotiation.

# File lib/strelka/httpresponse/negotiation.rb, line 79
def initialize( * )
        @mediatype_callbacks = {}
        @language_callbacks  = {}
        @encoding_callbacks  = {}

        @vary_fields         = Set.new

        super
end

Public Instance Methods

anchor
mimetype_map()

The Hash of symbolic mediatypes of the form:

{ <name (Symbol)> => <mimetype> }
# File lib/strelka/httpresponse/negotiation.rb, line 69
singleton_attr_reader :mimetype_map
anchor
negotiate()

Check for any negotiation that should happen and apply the necessary transforms if they're available.

# File lib/strelka/httpresponse/negotiation.rb, line 155
def negotiate
        return if !self.request
        self.transform_content_type
        self.transform_language
        self.transform_charset
        self.transform_encoding
end
anchor
negotiated_body()

Transform the entity body if it doesn't meet the criteria

# File lib/strelka/httpresponse/negotiation.rb, line 147
def negotiated_body
        self.negotiate
        return self.body
end
anchor
normalized_headers()

Overridden to add a Vary: header to outgoing headers if the response has any vary_fields.

# File lib/strelka/httpresponse/negotiation.rb, line 127
def normalized_headers
        headers = super

        unless self.vary_fields.empty?
                self.log.debug "Adding Vary header for %p" % [ self.vary_fields ]
                headers.vary = self.vary_fields.to_a.join( ', ' )
        end

        return headers
end
anchor
reset()

Overridden to reset content-negotiation callbacks, too.

# File lib/strelka/httpresponse/negotiation.rb, line 111
def reset
        super

        @mediatype_callbacks.clear
        @language_callbacks.clear
        @encoding_callbacks.clear

        # Not clearing the Vary: header for now, as it's useful in a 406 to
        # determine what accept-* headers can be modified to get an acceptable
        # response
        # @vary_fields.clear
end
anchor
stringifiers()

The Hash of stringification callbacks, keyed by mimetype.

# File lib/strelka/httpresponse/negotiation.rb, line 74
singleton_attr_reader :stringifiers
anchor
to_s()

Stringify the response – overridden to use the negotiated body.

# File lib/strelka/httpresponse/negotiation.rb, line 140
def to_s
        self.negotiate
        super
end

Acceptance Predicates

↑ top

Public Instance Methods

anchor
acceptable?()

Return true if the receiver satisfies all of its originating request's Accept* headers, or it's a bodiless response.

# File lib/strelka/httpresponse/negotiation.rb, line 170
def acceptable?
        # self.negotiate
        return self.bodiless? ||
               ( self.acceptable_content_type? &&
                 self.acceptable_charset? &&
                 self.acceptable_language? &&
                 self.acceptable_encoding? )
end
Also aliased as: is_acceptable?
anchor
acceptable_charset?()

Returns true if the receiver's charset is set to a value that was designated as acceptable by the originating request, or if there was no originating request.

# File lib/strelka/httpresponse/negotiation.rb, line 198
def acceptable_charset?
        req = self.request or return true
        charset = self.find_header_charset

        # Types other than text are binary, and so aren't subject to charset
        # acceptability.
        # For 'text/' subtypes:
        #   When no explicit charset parameter is provided by the sender, media
        #   subtypes of the "text" type are defined to have a default charset
        #   value of "ISO-8859-1" when received via HTTP. [RFC2616 3.7.1]
        if charset == Encoding::ASCII_8BIT
                return true unless self.content_type.start_with?( 'text/' )
                charset = Encoding::ISO8859_1
        end

        answer = req.accepts_charset?( charset )
        self.log.warn "Content-charset %p NOT acceptable: %p" %
                [ self.find_header_charset, req.accepted_charsets ] unless answer

        return answer
end
Also aliased as: has_acceptable_charset?
anchor
acceptable_content_type?()

Returns true if the content-type of the response is set to a mediatype that was designated as acceptable by the originating request, or if there was no originating request.

# File lib/strelka/httpresponse/negotiation.rb, line 184
def acceptable_content_type?
        req = self.request or return true
        answer = req.accepts?( self.content_type )
        self.log.warn "Content-type %p NOT acceptable: %p" %
                [ self.content_type, req.accepted_mediatypes ] unless answer

        return answer
end
Also aliased as: has_acceptable_content_type?
anchor
acceptable_encoding?()

Returns true if all of the receiver's encodings were designated as acceptable by the originating request, if there was no originating request, or if no encodings have been set.

# File lib/strelka/httpresponse/negotiation.rb, line 251
def acceptable_encoding?
        req = self.request or return true

        encs = self.encodings.dup
        encs << 'identity' if encs.empty?

        answer = encs.all? {|enc| req.accepts_encoding?(enc) }
        self.log.warn "Content-encoding %p NOT acceptable: %s" %
                [ encs, req.accepted_encodings ] unless answer

        return answer
end
Also aliased as: has_acceptable_encoding?
anchor
acceptable_language?()

Returns true if at least one of the receiver's languages is set to a value that was designated as acceptable by the originating request, if there was no originating request, or if no languages have been set for a non-empty entity body.

# File lib/strelka/httpresponse/negotiation.rb, line 226
def acceptable_language?
        req = self.request or return true

        # Lack of an accept-language field means all languages are accepted
        return true if req.accepted_languages.empty?

        # If no language is given for an existing entity body, there's no way
        # to know whether or not there's a better alternative
        return true if self.languages.empty?

        # If any of the languages present for the body are accepted, the
        # request is acceptable. Or at least that's what I got out of
        # reading RFC2616, Section 14.4.
        answer = self.languages.any? {|lang| req.accepts_language?(lang) }
        self.log.warn "Content-language %p NOT acceptable: %s" %
                [ self.languages, req.accepted_languages ] unless answer

        return answer
end
Also aliased as: has_acceptable_language?
anchor
has_acceptable_charset?()
Alias for: acceptable_charset?
anchor
has_acceptable_content_type?()
anchor
has_acceptable_encoding?()
anchor
has_acceptable_language?()
anchor
is_acceptable?()
Alias for: acceptable?

Charset negotiation callbacks

↑ top

Public Instance Methods

anchor
better_charsets()

Returns Strelka::HTTPRequest::Charset objects for accepted character sets that have a higher qvalue than the one used by the current response.

# File lib/strelka/httpresponse/negotiation.rb, line 466
def better_charsets
        req = self.request or return []
        return [] unless self.content_type &&
                self.content_type.start_with?( 'text/', 'application/' )
        return [] unless req.headers.accept_charset

        current_qvalue = 0.0
        charsets = req.accepted_charsets.sort
        current_charset = self.find_header_charset

        # If the current charset exists in the Accept-Charset: header, reset the current qvalue
        # to whatever its qvalue is
        if current_charset != Encoding::ASCII_8BIT
                charset = charsets.find {|mt| mt =~ current_charset }
                current_qvalue = charset.qvalue if charset
        end

        self.log.debug "Looking for better charsets than %p (%0.2f)" %
                [ current_charset, current_qvalue ]

        return charsets.sort.find_all do |cs|
                cs.qvalue > current_qvalue
        end
end
anchor
transcode_body( charset )

Try to transcode the entity body stream to one of the specified charsets. Returns the succesful Encoding object if transcoding succeeded, or nil if transcoding failed.

# File lib/strelka/httpresponse/negotiation.rb, line 519
def transcode_body( charset )
        unless enc = charset.encoding_object
                self.log.warn "    unsupported charset: %s" % [ charset ]
                return false
        end

        begin

                # StringIOs get their internal string transcoded directly
                if self.body.respond_to?( :string )
                        self.body.string.encode!( enc )
                        return true

                # For other IO objects, the situation is trickier -- we can't know that
                # encoding will succeed for more-restrictive charsets, so we only do
                # automatic transcoding if the 'wanted' one is a Unicode charset.
                # This probably isn't perfect, either.
                # :FIXME: Probably need a list of exceptions, i.e., charsets that don't
                # always transcode nicely into Unicode.
                elsif self.body.respond_to?( :fileno ) && UNICODE_CHARSETS.include?( enc )
                        self.log.info "Assuming %s data can be transcoded into %s" %
                                [ self.body.internal_encoding, enc ]

                        # Don't close the FD when this IO goes out of scope
                        oldbody = self.body
                        oldbody.autoclose = false

                        # Re-open the same file descriptor, but transcoding to the wanted encoding
                        self.body = IO.for_fd( oldbody.fileno, internal_encoding: enc )
                        return true
                end

        rescue Encoding::UndefinedConversionError => err
                self.log.error "%p while transcoding: %s" % [ err.class, err.message ]
        end

        return false
end
anchor
transform_charset()

Iterate over the originating request's acceptable charsets in qvalue+listed order, attempting to transcode the current entity body if it

# File lib/strelka/httpresponse/negotiation.rb, line 495
def transform_charset
        self.log.debug "Looking for charset transformations."
        if self.body.respond_to?( :string ) || self.body.respond_to?( :fileno )

                # Try each charset that's better than what we have already
                self.better_charsets.each do |charset|
                        self.log.debug "  trying to transcode to: %s" % [ charset ]

                        # If it succeeds, indicate that transcoding took place in the Vary header
                        if self.transcode_body( charset )
                                self.log.debug "  success; body is now %p" % [ charset ]
                                self.vary_fields.add( 'accept-charset' )
                                break
                        end
                end
        else
                self.log.warn "Don't know how to transcode a %p" % [ self.body.class ]
        end
end

Content-coding negotiation callbacks

↑ top

Public Instance Methods

anchor
better_encoding()

Returns Strelka::HTTPRequest::Encoding objects for accepted encodings that have a higher qvalue than the one used by the current response.

# File lib/strelka/httpresponse/negotiation.rb, line 583
def better_encoding
        req = self.request or return []
        return [] unless req.headers.accept_encoding

        current_qvalue = 0.0
        encodings = req.accepted_encodings.sort
        current_encodings = self.encodings.dup
        current_encodings.unshift( 'identity' )

        # Find the highest qvalue of the encodings that have been applied already
        current_qvalue = current_encodings.inject( current_qvalue ) do |qval, current_enc|
                qenc = encodings.find {|enc| enc =~ current_enc } or next qval
                qenc.qvalue > qval ? qenc.qvalue : qval
        end

        self.log.debug "Looking for better encodings than %p (%0.2f)" %
                [ current_encodings, current_qvalue ]

        return encodings.find_all do |enc|
                self.log.debug "  %s (%0.2f) > %0.2f?" % [ enc, enc.qvalue, current_qvalue ]
                enc.qvalue > current_qvalue
        end
end
anchor
for_encoding( *codings, &callback )

Register a callback that will be called during transparent content negotiation for the entity body if one or more of the specified codings is among the requested alternatives. The codings are Strings in the form described by RFC2616, section 3.5. The callback will be called with the coding name, and should return the new value for the entity body if it has transformed the bod. If successful, the response's body will be set to the new value, and the coding name added to the appropriate headers.

# File lib/strelka/httpresponse/negotiation.rb, line 571
def for_encoding( *codings, &callback )
        codings.each do |coding|
                self.encoding_callbacks[ coding ] = callback
        end

        # Include the 'Accept-Encoding:' header in the 'Vary:' header
        self.vary_fields.add( 'accept-encoding' )
end
anchor
transform_encoding()

Iterate over the originating request's acceptable encodings and apply each one in the order they were requested if they're available.

# File lib/strelka/httpresponse/negotiation.rb, line 610
def transform_encoding
        return if self.encoding_callbacks.empty?

        self.log.debug "Looking for acceptable content codings"
        self.better_encoding.each do |enc|
                self.log.debug "  looking for a callback for %p" % [ enc ]

                if (( callback = self.encoding_callbacks[enc.content_coding.to_sym] ))
                        self.log.debug "  trying callback %p for %p" %
                                [ callback, enc ]
                        if (( new_body = callback.call(enc.content_coding) ))
                                self.log.debug "    callback succeeded"
                                self.body = new_body
                                self.encodings << enc.content_coding
                                break
                        end
                elsif enc.content_coding == 'identity' && enc.qvalue.nonzero?
                        self.log.debug "  identity coding, no callback"
                        break
                end
        end
end

Content-type Callbacks

↑ top

Public Instance Methods

anchor
better_mediatypes()

Returns Strelka::HTTPRequest::MediaType objects for mediatypes that have a higher qvalue than the current response's entity body (if any).

# File lib/strelka/httpresponse/negotiation.rb, line 298
def better_mediatypes
        req = self.request or return []
        return [] unless req.headers.accept

        current_qvalue = 0.0
        mediatypes = req.accepted_mediatypes.sort

        # If the current mediatype exists in the Accept: header, reset the current qvalue
        # to whatever its qvalue is
        if self.content_type
                mediatype = mediatypes.find {|mt| mt =~ self.content_type }
                current_qvalue = mediatype.qvalue if mediatype
        end

        self.log.debug "Looking for better mediatypes than %p (%0.2f)" %
                [ self.content_type, current_qvalue ]

        return mediatypes.find_all do |mt|
                mt.qvalue > current_qvalue
        end
end
anchor
for( *mediatypes, &callback )

Register a callback that will be called during transparent content negotiation for the entity body if one or more of the specified mediatypes is among the requested alternatives. The mediatypes can be either mimetype Strings or Symbols that correspond to keys in the BUILTIN_MIMETYPES hash. The callback will be called with the desired mimetype, and should return the new value for the entity body if it successfully transformed the body, or a false value if the next alternative should be tried instead. If successful, the response's body will be set to the new value, its content_type set to the new mimetype, and its status changed to HTTP::OK.

# File lib/strelka/httpresponse/negotiation.rb, line 281
def for( *mediatypes, &callback )
        mediatypes.each do |mimetype|
                if mimetype.is_a?( Symbol )
                        mimetype = Strelka::HTTPResponse::Negotiation.mimetype_map[ mimetype ] or
                                raise "No known mimetype mapped to %p" % [ mimetype ]
                end

                self.mediatype_callbacks[ mimetype ] = callback
        end

        # Include the 'Accept:' header in the 'Vary:' header
        self.vary_fields.add( 'accept' )
end
anchor
transform_content_type()

Iterate over the originating request's acceptable content types in qvalue+listed order, looking for a content negotiation callback for each mediatype. If any are found, they are tried in declared order until one returns a true-ish value, which becomes the new entity body. If the body object is not a String,

# File lib/strelka/httpresponse/negotiation.rb, line 326
def transform_content_type
        self.log.debug "Applying content-type transforms (if any)"
        return if self.mediatype_callbacks.empty?

        self.log.debug "  transform callbacks registered: %p" % [ self.mediatype_callbacks ]
        self.better_mediatypes.each do |mediatype|
                callbacks = self.mediatype_callbacks.find_all do |mimetype, _|
                        mediatype =~ mimetype
                end

                if callbacks.empty?
                        self.log.debug "    no transforms for %s" % [ mediatype ]
                else
                        self.log.debug "    %d transform/s for %s" % [ callbacks.length, mediatype ]
                        callbacks.each do |mimetype, callback|
                                return if self.try_content_type_callback( mimetype, callback )
                        end
                end
        end
end
anchor
try_content_type_callback( mimetype, callback )

Attempt to apply the callback for the specified mediatype to the entity body, making the necessary changes to the request and returning true if the callback returns a new entity body, or returning a false value if it doesn't.

# File lib/strelka/httpresponse/negotiation.rb, line 351
def try_content_type_callback( mimetype, callback )
        self.log.debug "  trying content-type callback %p (%s)" % [ callback, mimetype ]

        new_body = begin
                callback.call( mimetype )
        rescue => err
                self.log.error "  %p raised from the callback for %s: %s" %
                        [ err.class, mimetype, err.message ]
                return false
        end

        self.log.debug "  successfully transformed: %p! Setting up response." % [ new_body.class ]
        stringifiers = Strelka::HTTPResponse::Negotiation.stringifiers
        if stringifiers.key?( mimetype )
                new_body = stringifiers[ mimetype ].call( new_body )
        else
                self.log.debug "    no stringifier registered for %p" % [ mimetype ]
        end

        self.body = new_body
        self.content_type = mimetype.dup # :TODO: Why is this frozen?
        self.status ||= HTTP::OK

        return true
end

Language negotiation callbacks

↑ top

Public Instance Methods

anchor
better_languages()

Returns Strelka::HTTPRequest::Language objects for natural languages that have a higher qvalue than the current response's entity body (if any).

# File lib/strelka/httpresponse/negotiation.rb, line 403
def better_languages
        req = self.request or return []

        current_qvalue = 0.0
        accepted_languages = req.accepted_languages.sort

        # If any of the current languages exists in the Accept-Language: header, reset
        # the current qvalue to the highest one among them
        unless self.languages.empty?
                current_qvalue = self.languages.reduce( current_qvalue ) do |qval, lang|
                        accepted_lang = accepted_languages.find {|alang| alang =~ lang } or
                                next qval
                        qval > accepted_lang.qvalue ? qval : accepted_lang.qvalue
                end
        end

        self.log.debug "Looking for better languages than %p (%0.2f)" %
                [ self.languages.join(', '), current_qvalue ]

        return accepted_languages.find_all do |lang|
                lang.qvalue > current_qvalue
        end
end
anchor
for_language( *language_tags, &callback )

Register a callback that will be called during transparent content negotiation for the entity body if one or more of the specified language_tags is among the requested alternatives. The language_tags are Strings in the form described by RFC2616, section 3.10. The callback will be called with the desired language code, and should return the new value for the entity body if it has value for the body, or a false value if the next alternative should be tried instead. If successful, the response's body will be set to the new value, and its status changed to HTTP::OK.

# File lib/strelka/httpresponse/negotiation.rb, line 391
def for_language( *language_tags, &callback )
        language_tags.flatten.each do |lang|
                self.language_callbacks[ lang.to_sym ] = callback
        end

        # Include the 'Accept-Language:' header in the 'Vary:' header
        self.vary_fields.add( 'accept-language' )
end
anchor
transform_language()

If there are any languages that have a higher qvalue than the one/s in languages, look for a negotiation callback that provides that language. If any are found, they are tried in declared order until one returns a true-ish value, which becomes the new entity body.

# File lib/strelka/httpresponse/negotiation.rb, line 432
def transform_language
        return if self.language_callbacks.empty?

        self.log.debug "Looking for language transformations"
        self.better_languages.uniq.each do |lang|
                callback = langcode = nil

                if lang.primary_tag
                        langcode = lang.language_range
                        callback = self.language_callbacks[ lang.primary_tag.to_sym ]
                else
                        langcode, callback = self.language_callbacks.first
                end

                next unless callback

                self.log.debug "  found a callback for %s: %p" % [ langcode, callback ]
                if (( new_body = callback.call(langcode) ))
                        self.body = new_body
                        self.languages.replace([ langcode.to_s ])
                        self.log.debug "    success."
                        break
                end

        end
end