(Quick Reference)

6.8 内容协商 - Reference Documentation

Authors: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith

Version: null

6.8 内容协商

Grails has built in support for Content negotiation using either the HTTP Accept header, an explicit format request parameter or the extension of a mapped URI.

Grails通过HTTP的Accept报头(显式的参数请求方式)或者扩展的URI映射来提供对内容协商的支持。

Configuring Mime Types

Before you can start dealing with content negotiation you need to tell Grails what content types you wish to support. By default Grails comes configured with a number of different content types within grails-app/conf/Config.groovy using the grails.mime.types setting:

grails.mime.types = [ xml: ['text/xml', 'application/xml'],
                      text: 'text-plain',
                      js: 'text/javascript',
                      rss: 'application/rss+xml',
                      atom: 'application/atom+xml',
                      css: 'text/css',
                      csv: 'text/csv',
                      all: '*/*',
                      json: 'text/json',
                      html: ['text/html','application/xhtml+xml']
                    ]

The above bit of configuration allows Grails to detect to format of a request containing either the 'text/xml' or 'application/xml' media types as simply 'xml'. You can add your own types by simply adding new entries into the map.

配置Mime类型

在你开始处理内容协商之前,你必须告诉Grails需要支持什么样的内容类型。缺省情况下,Grails将根据grails-app/conf/Config.groovy中的grails.mime.types设置来配置相关的内容类型:

grails.mime.types = [ xml: ['text/xml', 'application/xml'],
                      text: 'text-plain',
                      js: 'text/javascript',
                      rss: 'application/rss+xml',
                      atom: 'application/atom+xml',
                      css: 'text/css',
                      csv: 'text/csv',
                      all: '*/*',
                      json: 'text/json',
                      html: ['text/html','application/xhtml+xml']
                    ]

上述示例的配置块中,Grails将媒体类型为'text/xml'或'application/xml'的请求都只当做'xml'看待。你也可以添加自己的类型到类型为map的参数中。

Content Negotiation using the Accept header

Every incoming HTTP request has a special Accept header that defines what media types (or mime types) a client can "accept". In older browsers this is typically:

*/*

Which simply means anything. However, on newer browser something all together more useful is sent such as (an example of a Firefox Accept header):

text/xml, application/xml, application/xhtml+xml, text/html;q=0.9,
text/plain;q=0.8, image/png, */*;q=0.5

Grails parses this incoming format and adds a property to the response object that outlines the preferred response format. For the above example the following assertion would pass:

assert 'html' == response.format

Why? The text/html media type has the highest "quality" rating of 0.9, therefore is the highest priority. If you have an older browser as mentioned previously the result is slightly different:

assert 'all' == response.format

In this case 'all' possible formats are accepted by the client. To deal with different kinds of requests from Controllers you can use the withFormat method that acts as kind of a switch statement:

import grails.converters.XML

class BookController {

def list() { def books = Book.list() withFormat { html bookList: books js { render "alert('hello')" } xml { render books as XML } } } }

If the preferred format is html then Grails will execute the html() call only. This causes Grails to look for a view called either grails-app/views/books/list.html.gsp or grails-app/views/books/list.gsp. If the format is xml then the closure will be invoked and an XML response rendered.

How do we handle the "all" format? Simply order the content-types within your withFormat block so that whichever one you want executed comes first. So in the above example, "all" will trigger the html handler.

When using withFormat make sure it is the last call in your controller action as the return value of the withFormat method is used by the action to dictate what happens next.

使用Accept报头的内容协商

每一个发送的HTTP请求都有个特别的Accept报头,它定义了客户端能“接受”什么样的媒体类型(或mime类型)。这个在旧的浏览器中通常是:

*/*

用以简单的表示任何事物。然而,在较新的浏览器中,更多有用的信息将被起发送(比如一个Firefox的Accept报头):

text/xml, application/xml, application/xhtml+xml, text/html;q=0.9,
text/plain;q=0.8, image/png, */*;q=0.5

Grails解析这个输入格式,并为response对象添加一个优先响应此格式的property,比如上述示例,如下的断言将会通过:

assert 'html' == response.format

为什么呢?这个text/html媒体类型拥有的最高"质量"等级是0.9,因此,具有最高优先权。上述同样的示例如果在旧浏览器结果会有些稍微不同:

assert 'all' == response.format

此处的'all'格式是被客户端所接受的。 要在控制器中处理这些不同类型的请求,你可以使用withFormat方法,其跟switch语句类似:

import grails.converters.XML

class BookController {

def list() { def books = Book.list() withFormat { html bookList: books js { render "alert('hello')" } xml { render books as XML } } } }

如果优先格式是html,那么Grails将仅仅执行html()的调用。这将导致Grails查找名称为grails-app/views/books/list.html.gsp或者grails-app/views/books/list.gsp视图。 如果是xml格式,那么响应的必包将会被调用,并且渲染为一个XML响应。

那么我们该如何处理那个"all"格式呢?这要看你withFormat代码块中内容类型(content-types)的顺序了。以上述代码为例,"all"将触发html的处理。

在使用withFormat的时候,请确保它是控制器操作的最后一个调用,如此控制器才能知道下一步要做什么。

Request format vs. Response format

As of Grails 2.0, there is a separate notion of the request format and the response format. The request format is dictated by the CONTENT_TYPE header and is typically used to detect if the incoming request can be parsed into XML or JSON, whilst the response format uses the file extension, format parameter or ACCEPT header to attempt to deliver an appropriate response to the client.

The withFormat available on controllers deals specifically with the response format. If you wish to add logic that deals with the request format then you can do so using a separate withFormat method available on the request:

request.withFormat {
    xml {
        // read XML
    }
    json {
        // read JSON
    }
}

请求格式和响应格式

从Grails 2.0以来,就单独提出了 requestresponse 格式的概念。对于请求格式,通常是由CONTENT_TYPE报头决定的,并且用以检测收入的请求是否可以被解析为XML或者JSON。而响应格式通常是由文件扩展名、参数格式或者ACCEPT报头决定,并且尝试以合适的响应返回给客户端。

控制器的withFormat方法是针对响应格式而言的。如果你想增加请求格式的逻辑处理,需要单独使用request对象的withFormat方法:

request.withFormat {
    xml {
        // read XML
    }
    json {
        // read JSON
    }
}

Content Negotiation with the format Request Parameter

If fiddling with request headers if not your favorite activity you can override the format used by specifying a format request parameter:

/book/list?format=xml

You can also define this parameter in the URL Mappings definition:

"/book/list"(controller:"book", action:"list") {
    format = "xml"
}

请求参数格式的内容协商

如果不喜欢摆弄这些请求报头,你可以通过指定请求参数的format来覆盖这些格式:

/book/list?format=xml

你也可以将此参数定义在URL映射中,比如

"/book/list"(controller:"book", action:"list") {
    format = "xml"
}

Content Negotiation with URI Extensions

Grails also supports content negotiation using URI extensions. For example given the following URI:

/book/list.xml

Grails will remove the extension and map it to /book/list instead whilst simultaneously setting the content format to xml based on this extension. This behaviour is enabled by default, so if you wish to turn it off, you must set the grails.mime.file.extensions property in grails-app/conf/Config.groovy to false:

grails.mime.file.extensions = false

URI扩展的内容协商

Grails还提供了对扩展URI的内容协商的支持。比如下面URI示例:

/book/list.xml

Grails将移除其扩展后缀,而且将其映射为/book/list。与此同时,设置此内容格式为xml。缺省情况下,此行为是开启的。如果你想要关闭它,只需要设置grails-app/conf/Config.groovy中的grails.mime.file.extensions属性为false即可:

grails.mime.file.extensions = false

Testing Content Negotiation

To test content negotiation in a unit or integration test (see the section on Testing) you can either manipulate the incoming request headers:

void testJavascriptOutput() {
    def controller = new TestController()
    controller.request.addHeader "Accept",
              "text/javascript, text/html, application/xml, text/xml, */*"

controller.testAction() assertEquals "alert('hello')", controller.response.contentAsString }

Or you can set the format parameter to achieve a similar effect:

void testJavascriptOutput() {
    def controller = new TestController()
    controller.params.format = 'js'

controller.testAction() assertEquals "alert('hello')", controller.response.contentAsString }

测试内容协商

要在单元或者集成测试(请参考测试章节)中测试内容协商,你可以操作输入请求的报头方式进行,比如:

void testJavascriptOutput() {
    def controller = new TestController()
    controller.request.addHeader "Accept",
              "text/javascript, text/html, application/xml, text/xml, */*"

controller.testAction() assertEquals "alert('hello')", controller.response.contentAsString }

或者设置参数格式的方式来达到类似的效果:

void testJavascriptOutput() {
    def controller = new TestController()
    controller.params.format = 'js'

controller.testAction() assertEquals "alert('hello')", controller.response.contentAsString }