| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" |
| "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
| <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> |
| <head> |
| <!-- Matomo --> |
| <script> |
| var _paq = window._paq = window._paq || []; |
| /* tracker methods like "setCustomDimension" should be called before "trackPageView" */ |
| _paq.push(["setDoNotTrack", true]); |
| _paq.push(["disableCookies"]); |
| _paq.push(['trackPageView']); |
| _paq.push(['enableLinkTracking']); |
| (function() { |
| var u="https://analytics.apache.org/"; |
| _paq.push(['setTrackerUrl', u+'matomo.php']); |
| _paq.push(['setSiteId', '79']); |
| var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; |
| g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); |
| })(); |
| </script> |
| <!-- End Matomo Code --> |
| <title>9 Web Services 3.0.15</title> |
| <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> |
| <link rel="stylesheet" href="../css/main.css" type="text/css" media="screen, print" title="Style" charset="utf-8"/> |
| <link rel="stylesheet" href="../css/pdf.css" type="text/css" media="print" title="PDF" charset="utf-8"/> |
| <script type="text/javascript"> |
| function addJsClass() { |
| var classes = document.body.className.split(" "); |
| classes.push("js"); |
| document.body.className = classes.join(" "); |
| } |
| </script> |
| </head> |
| |
| <body class="body" onload="addJsClass();"> |
| <div id="navigation"> |
| <ul> |
| <li> |
| <div id="nav-summary" onmouseover="toggleNavSummary(false)" onmouseout="toggleNavSummary(true)"> |
| <a href="../guide/index.html" class="button">Table of contents</a> |
| |
| <div id="nav-summary-childs" style="display:none;"> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/introduction.html"><strong>1</strong><span>Introduction</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/gettingStarted.html"><strong>2</strong><span>Getting Started</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/upgrading.html"><strong>3</strong><span>Upgrading from Grails 2.x</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/conf.html"><strong>4</strong><span>Configuration</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/commandLine.html"><strong>5</strong><span>The Command Line</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/GORM.html"><strong>6</strong><span>Object Relational Mapping (GORM)</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/theWebLayer.html"><strong>7</strong><span>The Web Layer</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/traits.html"><strong>8</strong><span>Traits</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/webServices.html"><strong>9</strong><span>Web Services</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/async.html"><strong>10</strong><span>Asynchronous Programming</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/validation.html"><strong>11</strong><span>Validation</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/services.html"><strong>12</strong><span>The Service Layer</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/staticTypeCheckingAndCompilation.html"><strong>13</strong><span>Static Type Checking And Compilation</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/testing.html"><strong>14</strong><span>Testing</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/i18n.html"><strong>15</strong><span>Internationalization</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/security.html"><strong>16</strong><span>Security</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/plugins.html"><strong>17</strong><span>Plugins</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/spring.html"><strong>18</strong><span>Grails and Spring</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/hibernate.html"><strong>19</strong><span>Grails and Hibernate</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/scaffolding.html"><strong>20</strong><span>Scaffolding</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/deployment.html"><strong>21</strong><span>Deployment</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0"><a href="../guide/contributing.html"><strong>22</strong><span>Contributing to Grails</span></a> |
| </div> |
| |
| </div> |
| </div> |
| </li> |
| <li class="separator selected"> |
| <a id="ref-button" onclick="localToggle(); return false;" href="#">Quick Reference</a> |
| </li> |
| </ul> |
| </div> |
| <div id="header"> |
| <div class="images clearfix"> |
| |
| <span id="logo"><a href="http://grails.org" target="_blank"><img alt="Grails Logo" title="The Grails Framework" src="../img/grails.png" border="0"/></a></span> |
| |
| |
| </div> |
| <p>See the light - agile, industrial strength, rapid web application development made easy</p> |
| </div> |
| |
| |
| <table id="colset" border="0" cellpadding="0" cellspacing="0"> |
| <tr> |
| <td id="col1"> |
| <div id="main" class="corner-all"> |
| |
| |
| <div class="toc-item prev-left"><a href="../guide/traits.html"><< <strong>8</strong><span>Traits</span></a></div> |
| |
| |
| <span id='toggle-col1' class="toggle">(<a href="#" onclick="localToggle(); return false;">Quick Reference</a>)</span> |
| |
| |
| <div class="toc-item next-right"><a href="../guide/async.html"><strong>10</strong><span>Asynchronous Programming</span> >></a></div> |
| |
| |
| |
| <div class="project"> |
| <h1>9 Web Services - Reference Documentation</h1> |
| |
| <p><strong>Authors:</strong> Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith, Lari Hotari</p> |
| |
| <p><strong>Version:</strong> 3.0.15</p> |
| |
| |
| </div> |
| |
| |
| <div id="table-of-content"> |
| <h2>Table of Contents</h2> |
| |
| <div class="toc-item" style="margin-left:0px"><a href="#REST"><strong>9.1</strong><span>REST</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:10px"><a href="#domainResources"><strong>9.1.1</strong><span>Domain classes as REST resources</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:10px"><a href="#urlMappings"><strong>9.1.2</strong><span>Mapping to REST resources</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:10px"><a href="#linkingToResources"><strong>9.1.3</strong><span>Linking to REST resources</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:10px"><a href="#versioningResources"><strong>9.1.4</strong><span>Versioning REST resources</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:10px"><a href="#restfulControllers"><strong>9.1.5</strong><span>Implementing REST controllers</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#extendingRestfulController"><strong>9.1.5.1</strong><span>Extending the RestfulController super class</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#restControllersStepByStep"><strong>9.1.5.2</strong><span>Implementing REST Controllers Step by Step</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#generatingRestControllers"><strong>9.1.5.3</strong><span>Generating a REST controller using scaffolding</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:10px"><a href="#renderers"><strong>9.1.6</strong><span>Customizing Response Rendering</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#defaultRenderers"><strong>9.1.6.1</strong><span>Customizing the Default Renderers</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#objectMarshallers"><strong>9.1.6.2</strong><span>Registering Custom Objects Marshallers</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#namedObjectMarshallers"><strong>9.1.6.3</strong><span>Using Named Configurations for Object Marshallers</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#objectMarshallerInterface"><strong>9.1.6.4</strong><span>Implementing the ObjectMarshaller Interface</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#customRenderers"><strong>9.1.6.5</strong><span>Implementing a Custom Renderer</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#gspRenderers"><strong>9.1.6.6</strong><span>Using GSP to Customize Rendering</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:10px"><a href="#hypermedia"><strong>9.1.7</strong><span>Hypermedia as the Engine of Application State</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#hal"><strong>9.1.7.1</strong><span>HAL Support</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#atom"><strong>9.1.7.2</strong><span>Atom Support</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:20px"><a href="#vndError"><strong>9.1.7.3</strong><span>Vnd.Error Support</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:10px"><a href="#binding"><strong>9.1.8</strong><span>Customizing Binding of Resources</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0px"><a href="#SOAP"><strong>9.2</strong><span>SOAP</span></a> |
| </div> |
| |
| <div class="toc-item" style="margin-left:0px"><a href="#RSSAndAtom"><strong>9.3</strong><span>RSS and Atom</span></a> |
| </div> |
| |
| </div> |
| |
| |
| |
| <a name="8. Web Services"><!-- Legacy link --></a> |
| <h1 id="webServices">9 Web Services</h1> |
| Web Services are all about providing a web API onto your web application and are typically implemented in either <a href="http://en.wikipedia.org/wiki/Representational_State_Transfer" target="blank">REST</a> or <a href="http://en.wikipedia.org/wiki/SOAP." target="blank">SOAP</a> |
| |
| |
| <a name="8.1 REST"><!-- Legacy link --></a> |
| <h2 id="REST">9.1 REST</h2> |
| REST is not really a technology in itself, but more an architectural pattern. REST is very simple and just involves using plain XML or JSON as a communication medium, combined with URL patterns that are "representational" of the underlying system, and HTTP methods such as GET, PUT, POST and DELETE.<p class="paragraph"/>Each HTTP method maps to an action type. For example GET for retrieving data, POST for creating data, PUT for updating and so on.<p class="paragraph"/>Grails includes flexible features that make it easy to create RESTful APIs. Creating a RESTful resource can be as simple as one line of code, as demonstrated in the next section. |
| |
| |
| <h2 id="domainResources">9.1.1 Domain classes as REST resources</h2> |
| The easiest way to create a RESTful API in Grails is to expose a domain class as a REST resource. This can be done by adding the <code>grails.rest.Resource</code> transformation to any domain class:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.*<p class="paragraph"/>@Resource(uri='/books') |
| class Book {<p class="paragraph"/> <span class="java-object">String</span> title<p class="paragraph"/> <span class="java-keyword">static</span> constraints = { |
| title blank:<span class="java-keyword">false</span> |
| } |
| }</pre></div><p class="paragraph"/>Simply by adding the <code>Resource</code> transformation and specifying a URI, your domain class will automatically be available as a REST resource in either XML or JSON formats. The transformation will automatically register the necessary <a href="../guide/single.html#restfulMappings" class="guide">RESTful URL mapping</a> and create a controller called <code>BookController</code>.<p class="paragraph"/>You can try it out by adding some test data to <code>BootStrap.groovy</code>:<p class="paragraph"/><div class="code"><pre>def init = { servletContext -><p class="paragraph"/> <span class="java-keyword">new</span> Book(title:<span class="java-quote">"The Stand"</span>).save() |
| <span class="java-keyword">new</span> Book(title:<span class="java-quote">"The Shining"</span>).save() |
| }</pre></div><p class="paragraph"/>And then hitting the URL http://localhost:8080/myapp/books/1, which will render the response like:<p class="paragraph"/><div class="code"><pre><?xml version=<span class="java-quote">"1.0"</span> encoding=<span class="java-quote">"UTF-8"</span>?> |
| <book id=<span class="java-quote">"1"</span>> |
| <title>The Stand</title> |
| </book></pre></div><p class="paragraph"/>If you change the URL to <code>http://localhost:8080/myapp/books/1.json</code> you will get a JSON response such as:<p class="paragraph"/> |
| <div class="code"><pre>{<span class="java-quote">"id"</span>:1,<span class="java-quote">"title"</span>:<span class="java-quote">"The Stand"</span>}</pre></div><p class="paragraph"/>If you wish to change the default to return JSON instead of XML, you can do this by setting the <code>formats</code> attribute of the <code>Resource</code> transformation:<p class="paragraph"/> <div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.*<p class="paragraph"/>@Resource(uri='/books', formats=['json', 'xml']) |
| class Book { |
| … |
| }</pre></div><p class="paragraph"/>With the above example JSON will be prioritized. The list that is passed should contain the names of the formats that the resource should expose. The names of formats are defined in the <code>grails.mime.types</code> setting of <code>application.groovy</code>:<p class="paragraph"/><div class="code"><pre>grails.mime.types = [ |
| … |
| json: ['application/json', 'text/json'], |
| … |
| xml: ['text/xml', 'application/xml'] |
| ]</pre></div><p class="paragraph"/>See the section on <a href="../guide/single.html#contentNegotiation" class="guide">Configuring Mime Types</a> in the user guide for more information.<p class="paragraph"/>Instead of using the file extension in the URI, you can also obtain a JSON response using the ACCEPT header. Here's an example using the Unix <code>curl</code> tool:<p class="paragraph"/><div class="code"><pre>$ curl -i -H <span class="java-quote">"Accept: application/json"</span> localhost:8080/myapp/books/1 |
| {<span class="java-quote">"id"</span>:1,<span class="java-quote">"title"</span>:<span class="java-quote">"The Stand"</span>}</pre></div><p class="paragraph"/>This works thanks to Grails' <a href="../guide/single.html#contentNegotiation" class="guide">Content Negotiation</a> features.<p class="paragraph"/>You can create a new resource by issuing a <code>POST</code> request:<p class="paragraph"/><div class="code"><pre>$ curl -i -X POST -H <span class="java-quote">"Content-Type: application/json"</span> -d '{<span class="java-quote">"title"</span>:<span class="java-quote">"Along Came A Spider"</span>}' localhost:8080/myapp/books |
| HTTP/1.1 201 Created |
| Server: Apache-Coyote/1.1 |
| ...</pre></div><p class="paragraph"/>Updating can be done with a <code>PUT</code> request:<p class="paragraph"/><div class="code"><pre>$ curl -i -X PUT -H <span class="java-quote">"Content-Type: application/json"</span> -d '{<span class="java-quote">"title"</span>:<span class="java-quote">"Along Came A Spider"</span>}' localhost:8080/myapp/books/1 |
| HTTP/1.1 200 OK |
| Server: Apache-Coyote/1.1 |
| ...</pre></div><p class="paragraph"/>Finally a resource can be deleted with <code>DELETE</code> request:<p class="paragraph"/><div class="code"><pre>$ curl -i -X DELETE localhost:8080/myapp/books/1 |
| HTTP/1.1 204 No Content |
| Server: Apache-Coyote/1.1 |
| ...</pre></div><p class="paragraph"/>As you can see, the <code>Resource</code> transformation enables all of the HTTP method verbs on the resource. You can enable only read-only capabilities by setting the <code>readOnly</code> attribute to true:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.*<p class="paragraph"/>@Resource(uri='/books', readOnly=<span class="java-keyword">true</span>) |
| class Book { |
| … |
| }</pre></div><p class="paragraph"/>In this case POST, PUT and DELETE requests will be forbidden. |
| |
| |
| |
| <h2 id="urlMappings">9.1.2 Mapping to REST resources</h2> |
| If you prefer to keep the declaration of the URL mapping in your <code>UrlMappings.groovy</code> file then simply removing the <code>uri</code> attribute of the <code>Resource</code> transformation and adding the following line to <code>UrlMappings.groovy</code> will suffice:<p class="paragraph"/><div class="code"><pre><span class="java-quote">"/books"</span>(resources:<span class="java-quote">"book"</span>)</pre></div><p class="paragraph"/>Extending your API to include more end points then becomes trivial:<p class="paragraph"/><div class="code"><pre><span class="java-quote">"/books"</span>(resources:<span class="java-quote">"book"</span>) { |
| <span class="java-quote">"/publisher"</span>(controller:<span class="java-quote">"publisher"</span>, method:<span class="java-quote">"GET"</span>) |
| }</pre></div><p class="paragraph"/>The above example will expose the URI <code>/books/1/publisher</code>.<p class="paragraph"/>A more detailed explanation on <a href="../guide/single.html#restfulMappings" class="guide">creating RESTful URL mappings</a> can be found in the <a href="../guide/single.html#urlMappings" class="guide">URL Mappings section</a> of the user guide. |
| |
| |
| <h2 id="linkingToResources">9.1.3 Linking to REST resources</h2> |
| The <code>link</code> tag offers an easy way to link to any domain class resource:<p class="paragraph"/><div class="code"><pre><g:link resource=<span class="java-quote">"${book}"</span>>My Link</g:link></pre></div><p class="paragraph"/>However, currently you cannot use g:link to link to the DELETE action and most browsers do not support sending the DELETE method directly.<p class="paragraph"/>The best way to accomplish this is to use a form submit:<p class="paragraph"/><div class="code"><pre><form action=<span class="java-quote">"/book/2"</span> method=<span class="java-quote">"post"</span>> |
| <input type=<span class="java-quote">"hidden"</span> name=<span class="java-quote">"_method"</span> value=<span class="java-quote">"DELETE"</span>/> |
| </form></pre></div><p class="paragraph"/>Grails supports overriding the request method via the hidden _method parameter. This is for browser compatibility purposes. This is useful when using restful resource mappings to create powerful web interfaces. |
| To make a link fire this type of event, perhaps capture all click events for links with a `data-method` attribute and issue a form submit via javascript. |
| |
| |
| |
| <h2 id="versioningResources">9.1.4 Versioning REST resources</h2> |
| A common requirement with a REST API is to expose different versions at the same time. There are a few ways this can be achieved in Grails.<p class="paragraph"/><h4>Versioning using the URI</h4><p class="paragraph"/>A common approach is to use the URI to version APIs (although this approach is discouraged in favour of Hypermedia). For example, you can define the following URL mappings:<p class="paragraph"/><div class="code"><pre><span class="java-quote">"/books/v1"</span>(resources:<span class="java-quote">"book"</span>, namespace:'v1') |
| <span class="java-quote">"/books/v2"</span>(resources:<span class="java-quote">"book"</span>, namespace:'v2')</pre></div><p class="paragraph"/>That will match the following controllers:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">package</span> myapp.v1<p class="paragraph"/>class BookController { |
| <span class="java-keyword">static</span> namespace = 'v1' |
| }<p class="paragraph"/><span class="java-keyword">package</span> myapp.v2<p class="paragraph"/>class BookController { |
| <span class="java-keyword">static</span> namespace = 'v2' |
| }</pre></div><p class="paragraph"/>This approach has the disadvantage of requiring two different URI namespaces for your API.<p class="paragraph"/><h4>Versioning with the Accept-Version header</h4><p class="paragraph"/>As an alternative Grails supports the passing of an <code>Accept-Version</code> header from clients. For example you can define the following URL mappings:<p class="paragraph"/><div class="code"><pre><span class="java-quote">"/books"</span>(version:'1.0', resources:<span class="java-quote">"book"</span>, namespace:'v1') |
| <span class="java-quote">"/books"</span>(version:'2.0', resources:<span class="java-quote">"book"</span>, namespace:'v2')</pre></div><p class="paragraph"/> |
| Then in the client simply pass which version you need using the <code>Accept-Version</code> header:<p class="paragraph"/><div class="code"><pre>$ curl -i -H <span class="java-quote">"Accept-Version: 1.0"</span> -X GET http://localhost:8080/myapp/books</pre></div><p class="paragraph"/><h4>Versioning using Hypermedia / Mime Types</h4><p class="paragraph"/>Another approach to versioning is to use Mime Type definitions to declare the version of your custom media types (see the section on "Hypermedia as the Engine of Application State" for more information about Hypermedia concepts). For example, in <code>application.groovy</code> you can declare a custom Mime Type for your resource that includes a version parameter (the 'v' parameter):<p class="paragraph"/><div class="code"><pre>grails.mime.types = [ |
| all: '*/*', |
| book: <span class="java-quote">"application/vnd.books.org.book+json;v=1.0"</span>, |
| bookv2: <span class="java-quote">"application/vnd.books.org.book+json;v=2.0"</span>, |
| … |
| }</pre></div><p class="paragraph"/><blockquote class="warning"> |
| It is critical that place your new mime types after the 'all' Mime Type because if the Content Type of the request cannot be established then the first entry in the map is used for the response. If you have your new Mime Type at the top then Grails will always try and send back your new Mime Type if the requested Mime Type cannot be established. |
| </blockquote><p class="paragraph"/>Then override the renderer (see the section on "Customizing Response Rendering" for more information on custom renderers) to send back the custom Mime Type in <code>grails-app/conf/spring/resourses.groovy</code>:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.render.json.* |
| <span class="java-keyword">import</span> grails.web.mime.*<p class="paragraph"/>beans = { |
| bookRendererV1(JsonRenderer, myapp.v1.Book, <span class="java-keyword">new</span> MimeType(<span class="java-quote">"application/vnd.books.org.book+json"</span>, [v:<span class="java-quote">"1.0"</span>])) |
| bookRendererV2(JsonRenderer, myapp.v2.Book, <span class="java-keyword">new</span> MimeType(<span class="java-quote">"application/vnd.books.org.book+json"</span>, [v:<span class="java-quote">"2.0"</span>])) |
| }</pre></div><p class="paragraph"/>Then update the list of acceptable response formats in your controller:<p class="paragraph"/><div class="code"><pre>class BookController <span class="java-keyword">extends</span> RestfulController { |
| <span class="java-keyword">static</span> responseFormats = ['json', 'xml', 'book', 'bookv2']<p class="paragraph"/> // … |
| }</pre></div><p class="paragraph"/> |
| Then using the <code>Accept</code> header you can specify which version you need using the Mime Type:<p class="paragraph"/><div class="code"><pre>$ curl -i -H <span class="java-quote">"Accept: application/vnd.books.org.book+json;v=1.0"</span> -X GET http://localhost:8080/myapp/books</pre></div> |
| |
| |
| |
| <h2 id="restfulControllers">9.1.5 Implementing REST controllers</h2> |
| The <code>Resource</code> transformation is a quick way to get started, but typically you'll want to customize the controller logic, the rendering of the response or extend the API to include additional actions. |
| |
| |
| <h2 id="extendingRestfulController">9.1.5.1 Extending the RestfulController super class</h2> |
| The easiest way to get started doing so is to create a new controller for your resource that extends the <code>grails.rest.RestfulController</code> super class. For example:<p class="paragraph"/><div class="code"><pre>class BookController <span class="java-keyword">extends</span> RestfulController { |
| <span class="java-keyword">static</span> responseFormats = ['json', 'xml'] |
| BookController() { |
| <span class="java-keyword">super</span>(Book) |
| } |
| }</pre></div><p class="paragraph"/>To customize any logic you can just override the appropriate action. The following table provides the names of the action names and the URIs they map to:<p class="paragraph"/><table class="wiki-table" cellpadding="0" cellspacing="0" border="0"><tr><th>HTTP Method</th><th>URI</th><th>Controller Action</th></tr><tr class="table-odd"><td>GET</td><td>/books</td><td>index</td></tr><tr class="table-even"><td>GET</td><td>/books/create</td><td>create</td></tr><tr class="table-odd"><td>POST</td><td>/books</td><td>save</td></tr><tr class="table-even"><td>GET</td><td>/books/${id}</td><td>show</td></tr><tr class="table-odd"><td>GET</td><td>/books/${id}/edit</td><td>edit</td></tr><tr class="table-even"><td>PUT</td><td>/books/${id}</td><td>update</td></tr><tr class="table-odd"><td>DELETE</td><td>/books/${id}</td><td>delete</td></tr></table><p class="paragraph"/><blockquote class="note"> |
| Note that the <code>create</code> and <code>edit</code> actions are only needed if the controller exposes an HTML interface. |
| </blockquote><p class="paragraph"/>As an example, if you have a <a href="../guide/single.html#restfulMappings" class="guide">nested resource</a> then you would typically want to query both the parent and the child identifiers. For example, given the following URL mapping:<p class="paragraph"/><div class="code"><pre><span class="java-quote">"/authors"</span>(resources:'author') { |
| <span class="java-quote">"/books"</span>(resources:'book') |
| }</pre></div><p class="paragraph"/>You could implement the nested controller as follows:<p class="paragraph"/><div class="code"><pre>class BookController <span class="java-keyword">extends</span> RestfulController { |
| <span class="java-keyword">static</span> responseFormats = ['json', 'xml'] |
| BookController() { |
| <span class="java-keyword">super</span>(Book) |
| }<p class="paragraph"/> @Override |
| <span class="java-keyword">protected</span> Book queryForResource(Serializable id) { |
| Book.where { |
| id == id && author.id = params.authorId |
| }.find() |
| }<p class="paragraph"/>}</pre></div><p class="paragraph"/>The example above subclasses <code>RestfulController</code> and overrides the protected <code>queryForResource</code> method to customize the query for the resource to take into account the parent resource.<p class="paragraph"/><h4>Customizing Data Binding In A RestfulController Subclass</h4><p class="paragraph"/>The RestfulController class contains code which does data binding for actions like <code>save</code> and <code>update</code>. The class defines a <code>getObjectToBind()</code> method which returns a value which will be used as the source for data binding. For example, the update action does something like this...<p class="paragraph"/><div class="code"><pre>class RestfulController<T> {<p class="paragraph"/> def update() { |
| T instance = // retrieve instance from the database...<p class="paragraph"/> instance.properties = getObjectToBind()<p class="paragraph"/> // … |
| }<p class="paragraph"/> // … |
| }</pre></div><p class="paragraph"/>By default the <code>getObjectToBind()</code> method returns the <a href="../ref/Controllers/request.html" class="controllers">request</a> object. When the <code>request</code> object is used as the binding source, if the request has a body then the body will be parsed and its contents will be used to do the data binding, otherwise the request parameters will be used to do the data binding. Subclasses of RestfulController may override the <code>getObjectToBind()</code> method and return anything that is a valid binding source, including a <a href="http://docs.oracle.com/javase/6/docs/api/java/util/Map.html" class="api">Map</a> or a <a href="https://grails.apache.org/docs/3.0.x/api/org/grails/databinding/DataBindingSource.html" class="api">DataBindingSource</a>. For most use cases binding the request is appropriate but the <code>getObjectToBind()</code> method allows for changing that behavior where desired.<p class="paragraph"/><h4>Using custom subclass of RestfulController with Resource annotation</h4><p class="paragraph"/>You can also customize the behaviour of the controller that backs the Resource annotation.<p class="paragraph"/>The class must provide a constructor that takes a domain class as it's argument. The second constructor is required for supporting Resource annotation with readOnly=true.<p class="paragraph"/>This is a template that can be used for subclassed RestfulController classes used in Resource annotations: |
| <div class="code"><pre>class SubclassRestfulController<T> <span class="java-keyword">extends</span> RestfulController<T> { |
| SubclassRestfulController(<span class="java-object">Class</span><T> domainClass) { |
| <span class="java-keyword">this</span>(domainClass, <span class="java-keyword">false</span>) |
| }<p class="paragraph"/> SubclassRestfulController(<span class="java-object">Class</span><T> domainClass, <span class="java-object">boolean</span> readOnly) { |
| <span class="java-keyword">super</span>(domainClass, readOnly) |
| } |
| }</pre></div><p class="paragraph"/> |
| You can specify the super class of the controller that backs the Resource annotation with the <code>superClass</code> attribute.<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.*<p class="paragraph"/>@Resource(uri='/books', superClass=SubclassRestfulController) |
| class Book {<p class="paragraph"/> <span class="java-object">String</span> title<p class="paragraph"/> <span class="java-keyword">static</span> constraints = { |
| title blank:<span class="java-keyword">false</span> |
| } |
| }</pre></div> |
| |
| |
| |
| <h2 id="restControllersStepByStep">9.1.5.2 Implementing REST Controllers Step by Step</h2> |
| If you don't want to take advantage of the features provided by the <code>RestfulController</code> super class, then you can implement each HTTP verb yourself manually. The first step is to create a controller:<p class="paragraph"/><div class="code"><pre>$ grails create-controller book</pre></div><p class="paragraph"/>Then add some useful imports and enable readOnly by default:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.transaction.* |
| <span class="java-keyword">import</span> <span class="java-keyword">static</span> org.springframework.http.HttpStatus.* |
| <span class="java-keyword">import</span> <span class="java-keyword">static</span> org.springframework.http.HttpMethod.*<p class="paragraph"/>@Transactional(readOnly = <span class="java-keyword">true</span>) |
| class BookController { |
| … |
| }</pre></div><p class="paragraph"/>Recall that each HTTP verb matches a particular Grails action according to the following conventions:<p class="paragraph"/><table class="wiki-table" cellpadding="0" cellspacing="0" border="0"><tr><th>HTTP Method</th><th>URI</th><th>Controller Action</th></tr><tr class="table-odd"><td>GET</td><td>/books</td><td>index</td></tr><tr class="table-even"><td>GET</td><td>/books/${id}</td><td>show</td></tr><tr class="table-odd"><td>GET</td><td>/books/create</td><td>create</td></tr><tr class="table-even"><td>GET</td><td>/books/${id}/edit</td><td>edit</td></tr><tr class="table-odd"><td>POST</td><td>/books</td><td>save</td></tr><tr class="table-even"><td>PUT</td><td>/books/${id}</td><td>update</td></tr><tr class="table-odd"><td>DELETE</td><td>/books/${id}</td><td>delete</td></tr></table><p class="paragraph"/><blockquote class="note"> |
| The 'create' and 'edit' actions are already required if you plan to implement an HTML interface for the REST resource. They are there in order to render appropriate HTML forms to create and edit a resource. If this is not a requirement they can be discarded. |
| </blockquote><p class="paragraph"/>The key to implementing REST actions is the <a href="../ref/Controllers/respond.html" class="controllers">respond</a> method introduced in Grails 2.3. The <code>respond</code> method tries to produce the most appropriate response for the requested content type (JSON, XML, HTML etc.)<p class="paragraph"/><h4>Implementing the 'index' action</h4><p class="paragraph"/>For example, to implement the <code>index</code> action, simply call the <code>respond</code> method passing the list of objects to respond with:<p class="paragraph"/><div class="code"><pre>def index(<span class="java-object">Integer</span> max) { |
| params.max = <span class="java-object">Math</span>.min(max ?: 10, 100) |
| respond Book.list(params), model:[bookCount: Book.count()] |
| }</pre></div><p class="paragraph"/>Note that in the above example we also use the <code>model</code> argument of the <code>respond</code> method to supply the total count. This is only required if you plan to support pagination via some user interface.<p class="paragraph"/>The <code>respond</code> method will, using <a href="../guide/single.html#contentNegotiation" class="guide">Content Negotiation</a>, attempt to reply with the most appropriate response given the content type requested by the client (via the ACCEPT header or file extension).<p class="paragraph"/>If the content type is established to be HTML then a model will be produced such that the action above would be the equivalent of writing:<p class="paragraph"/><div class="code"><pre>def index(<span class="java-object">Integer</span> max) { |
| params.max = <span class="java-object">Math</span>.min(max ?: 10, 100) |
| [bookList: Book.list(params), bookCount: Book.count()] |
| }</pre></div><p class="paragraph"/>By providing an <code>index.gsp</code> file you can render an appropriate view for the given model. If the content type is something other than HTML then the <code>respond</code> method will attempt to lookup an appropriate <code>grails.rest.render.Renderer</code> instance that is capable of rendering the passed object. This is done by inspecting the <code>grails.rest.render.RendererRegistry</code>.<p class="paragraph"/>By default there are already renderers configured for JSON and XML, to find out how to register a custom renderer see the section on "Customizing Response Rendering".<p class="paragraph"/><h4>Implementing the 'show' action</h4><p class="paragraph"/>The <code>show</code> action, which is used to display and individual resource by id, can be implemented in one line of Groovy code (excluding the method signature):<p class="paragraph"/><div class="code"><pre>def show(Book book) { |
| respond book |
| }</pre></div><p class="paragraph"/>By specifying the domain instance as a parameter to the action Grails will automatically attempt to lookup the domain instance using the <code>id</code> parameter of the request. If the domain instance doesn't exist, then <code>null</code> will be passed into the action. The <code>respond</code> method will return a 404 error if null is passed otherwise once again it will attempt to render an appropriate response. If the format is HTML then an appropriate model will produced. The following action is functionally equivalent to the above action:<p class="paragraph"/><div class="code"><pre>def show(Book book) { |
| <span class="java-keyword">if</span>(book == <span class="java-keyword">null</span>) { |
| render status:404 |
| } |
| <span class="java-keyword">else</span> { |
| <span class="java-keyword">return</span> [book: book] |
| } |
| }</pre></div><p class="paragraph"/><h4>Implementing the 'save' action</h4><p class="paragraph"/>The <code>save</code> action creates new resource representations. To start off, simply define an action that accepts a resource as the first argument and mark it as <code>Transactional</code> with the <code>grails.transaction.Transactional</code> transform:<p class="paragraph"/><div class="code"><pre>@Transactional |
| def save(Book book) { |
| … |
| }</pre></div><p class="paragraph"/>Then the first thing to do is check whether the resource has any <a href="../guide/single.html#validation" class="guide">validation errors</a> and if so respond with the errors:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">if</span>(book.hasErrors()) { |
| respond book.errors, view:'create' |
| } |
| <span class="java-keyword">else</span> { |
| … |
| }</pre></div><p class="paragraph"/>In the case of HTML the 'create' view will be rendered again so the user can correct the invalid input. In the case of other formats (JSON, XML etc.), the errors object itself will be rendered in the appropriate format and a status code of 422 (UNPROCESSABLE_ENTITY) returned.<p class="paragraph"/>If there are no errors then the resource can be saved and an appropriate response sent:<p class="paragraph"/><div class="code"><pre>book.save flush:<span class="java-keyword">true</span> |
| withFormat { |
| html { |
| flash.message = message(code: '<span class="java-keyword">default</span>.created.message', args: [message(code: 'book.label', <span class="java-keyword">default</span>: 'Book'), book.id]) |
| redirect book |
| } |
| '*' { render status: CREATED } |
| }</pre></div><p class="paragraph"/>In the case of HTML a redirect is issued to the originating resource and for other formats a status code of 201 (CREATED) is returned.<p class="paragraph"/><h4>Implementing the 'update' action</h4><p class="paragraph"/> |
| The <code>update</code> action updates an existing resource representations and is largely similar to the <code>save</code> action. First define the method signature:<p class="paragraph"/><div class="code"><pre>@Transactional |
| def update(Book book) { |
| … |
| }</pre></div><p class="paragraph"/>If the resource exists then Grails will load the resource, otherwise null we passed. In the case of null, you should return a 404:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">if</span>(book == <span class="java-keyword">null</span>) { |
| render status: NOT_FOUND |
| } |
| <span class="java-keyword">else</span> { |
| … |
| }</pre></div><p class="paragraph"/>Then once again check for errors <a href="../guide/single.html#validation" class="guide">validation errors</a> and if so respond with the errors:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">if</span>(book.hasErrors()) { |
| respond book.errors, view:'edit' |
| } |
| <span class="java-keyword">else</span> { |
| … |
| }</pre></div><p class="paragraph"/>In the case of HTML the 'edit' view will be rendered again so the user can correct the invalid input. In the case of other formats (JSON, XML etc.) the errors object itself will be rendered in the appropriate format and a status code of 422 (UNPROCESSABLE_ENTITY) returned.<p class="paragraph"/>If there are no errors then the resource can be saved and an appropriate response sent:<p class="paragraph"/><div class="code"><pre>book.save flush:<span class="java-keyword">true</span> |
| withFormat { |
| html { |
| flash.message = message(code: '<span class="java-keyword">default</span>.updated.message', args: [message(code: 'book.label', <span class="java-keyword">default</span>: 'Book'), book.id]) |
| redirect book |
| } |
| '*' { render status: OK } |
| }</pre></div><p class="paragraph"/>In the case of HTML a redirect is issued to the originating resource and for other formats a status code of 200 (OK) is returned.<p class="paragraph"/><h4>Implementing the 'delete' action</h4><p class="paragraph"/>The <code>delete</code> action deletes an existing resource. The implementation is largely similar to the <code>update</code> action, expect the <code>delete()</code> method is called instead:<p class="paragraph"/><div class="code"><pre>book.delete flush:<span class="java-keyword">true</span> |
| withFormat { |
| html { |
| flash.message = message(code: '<span class="java-keyword">default</span>.deleted.message', args: [message(code: 'Book.label', <span class="java-keyword">default</span>: 'Book'), book.id]) |
| redirect action:<span class="java-quote">"index"</span>, method:<span class="java-quote">"GET"</span> |
| } |
| '*'{ render status: NO_CONTENT } |
| }</pre></div><p class="paragraph"/>Notice that for an HTML response a redirect is issued back to the <code>index</code> action, whilst for other content types a response code 204 (NO_CONTENT) is returned.<p class="paragraph"/> |
| |
| |
| |
| <h2 id="generatingRestControllers">9.1.5.3 Generating a REST controller using scaffolding</h2> |
| To see some of these concepts in action and help you get going the <a href="http://grails.org/plugin/scaffolding" target="blank">Scaffolding plugin</a>, version 2.0 and above, can generate a REST ready controller for you, simply run the command:<p class="paragraph"/><div class="code"><pre>$ grails generate-controller [Domain <span class="java-object">Class</span> Name]</pre></div> |
| |
| |
| <h2 id="renderers">9.1.6 Customizing Response Rendering</h2> |
| There are several ways to customize response rendering in Grails. |
| |
| |
| |
| <h2 id="defaultRenderers">9.1.6.1 Customizing the Default Renderers</h2> |
| The default renderers for XML and JSON can be found in the <code>grails.rest.render.xml</code> and <code>grails.rest.render.json</code> packages respectively. These use the Grails converters (<code>grails.converters.XML</code> and <code>grails.converters.JSON</code>) by default for response rendering.<p class="paragraph"/>You can easily customize response rendering using these default renderers. A common change you may want to make is to include or exclude certain properties from rendering.<p class="paragraph"/><h4>Including or Excluding Properties from Rendering</h4><p class="paragraph"/>As mentioned previously, Grails maintains a registry of <code>grails.rest.render.Renderer</code> instances. There are some default configured renderers and the ability to register or override renderers for a given domain class or even for a collection of domain classes. To include a particular property from rendering you need to register a custom renderer by defining a bean in <code>grails-app/conf/spring/resources.groovy</code>:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.render.xml.*<p class="paragraph"/>beans = { |
| bookRenderer(XmlRenderer, Book) { |
| includes = ['title'] |
| } |
| }</pre></div><p class="paragraph"/> |
| <blockquote class="note"> |
| The bean name is not important (Grails will scan the application context for all registered renderer beans), but for organizational and readability purposes it is recommended you name it something meaningful. |
| </blockquote><p class="paragraph"/>To exclude a property, the <code>excludes</code> property of the <code>XmlRenderer</code> class can be used:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.render.xml.*<p class="paragraph"/>beans = { |
| bookRenderer(XmlRenderer, Book) { |
| excludes = ['isbn'] |
| } |
| }</pre></div><p class="paragraph"/><h4>Customizing the Converters</h4><p class="paragraph"/>As mentioned previously, the default renders use the <code>grails.converters</code> package under the covers. In other words, under the covers they essentially do the following:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.converters.*<p class="paragraph"/>… |
| render book as XML<p class="paragraph"/>// or render book as JSON</pre></div><p class="paragraph"/>Why the separation between converters and renderers? Well a renderer has more flexibility to use whatever rendering technology you chose. When implementing a custom renderer you could use <a href="http://wiki.fasterxml.com/JacksonHome" target="blank">Jackson</a>, <a href="http://code.google.com/p/google-gson/" target="blank">Gson</a> or any Java library to implement the renderer. Converters on the other hand are very much tied to Grails' own marshalling implementation. |
| |
| |
| |
| <h2 id="objectMarshallers">9.1.6.2 Registering Custom Objects Marshallers</h2> |
| Grails' Converters feature the notion of an <a href="https://grails.apache.org/docs/3.0.x/api/org/grails/web/converters/marshaller/ObjectMarshaller.html" class="api">ObjectMarshaller</a> and each type can have a registered <code>ObjectMarshaller</code>. You can register custom <code>ObjectMarshaller</code> instances to completely customize response rendering. For example, you can define the following in <code>BootStrap.init</code>:<p class="paragraph"/><div class="code"><pre>XML.registerObjectMarshaller Book, { Book book, XML xml -> |
| xml.attribute 'id', book.id |
| xml.build { |
| title(book.title) |
| } |
| }</pre></div><p class="paragraph"/>You can customize the formatting of an individual value this way too. For example the <a href="http://grails.org/plugin/jodatime" target="blank">JodaTime plugin</a> does the following to support rendering of JodaTime dates in JSON output:<p class="paragraph"/><div class="code"><pre>JSON.registerObjectMarshaller(DateTime) { |
| <span class="java-keyword">return</span> it?.toString(<span class="java-quote">"yyyy-MM-dd'T'HH:mm:ss'Z'"</span>) |
| }</pre></div><p class="paragraph"/>In the case of JSON it's often simple to use a map to customize output:<p class="paragraph"/><div class="code"><pre>JSON.registerObjectMarshaller(Book) { |
| def map= [:] |
| map['titl'] = it.title |
| map['auth'] = it.author |
| <span class="java-keyword">return</span> map |
| }</pre></div><p class="paragraph"/><h4>Registering Custom Marshallers via Spring</h4><p class="paragraph"/>Note that if you have many custom marshallers it is recommended you split the registration of these into a separate class:<p class="paragraph"/><div class="code"><pre>class CustomMarshallerRegistrar {<p class="paragraph"/> @javax.annotation.PostConstruct |
| void registerMarshallers() { |
| JSON.registerObjectMarshaller(DateTime) { |
| <span class="java-keyword">return</span> it?.toString(<span class="java-quote">"yyyy-MM-dd'T'HH:mm:ss'Z'"</span>) |
| } |
| } |
| }</pre></div><p class="paragraph"/>Then define this class as Spring bean in <code>grails-app/conf/spring/resources.groovy</code>:<p class="paragraph"/><div class="code"><pre>beans = { |
| myCustomMarshallerRegistrar(CustomMarshallerRegistrar) |
| }</pre></div><p class="paragraph"/>The <code>PostConstruct</code> annotation will get triggered on startup of your application. |
| |
| |
| |
| <h2 id="namedObjectMarshallers">9.1.6.3 Using Named Configurations for Object Marshallers</h2> |
| It is also possible to register named configurations. For example:<p class="paragraph"/><div class="code"><pre>XML.createNamedConfig('publicApi') { |
| it.registerObjectMarshaller(Book) { Book book, XML xml -> |
| // <span class="java-keyword">do</span> <span class="java-keyword">public</span> API |
| } |
| } |
| XML.createNamedConfig('adminApi') { |
| it.registerObjectMarshaller(Book) { Book book, XML xml -> |
| // <span class="java-keyword">do</span> admin API |
| } |
| }</pre></div><p class="paragraph"/>Then when you use either the <code>render</code> or <code>respond</code> methods you can wrap the call in a named configuration if necessary to customize rendering per request:<p class="paragraph"/><div class="code"><pre>XML.use( isAdmin ? 'adminApi' : 'publicApi') { |
| render book as XML |
| }</pre></div><p class="paragraph"/>or<p class="paragraph"/><div class="code"><pre>XML.use( isAdmin ? 'adminApi' : 'publicApi') { |
| respond book |
| }</pre></div> |
| |
| |
| <h2 id="objectMarshallerInterface">9.1.6.4 Implementing the ObjectMarshaller Interface</h2> |
| For more complex marshallers it is recommended you implement the <a href="https://grails.apache.org/docs/3.0.x/api/org/grails/web/converters/marshaller/ObjectMarshaller.html" class="api">ObjectMarshaller</a> interface. For example given a domain class:<p class="paragraph"/><div class="code"><pre>class Book { |
| <span class="java-object">String</span> title |
| }</pre></div><p class="paragraph"/>By default the output when using:<p class="paragraph"/><div class="code"><pre>render book as XML</pre></div><p class="paragraph"/>Would look like:<p class="paragraph"/><div class="code"><pre><span class="xml-tag"><book id=<span class="xml-quote">"1"</span>></span> |
| <span class="xml-tag"><title></span>The Stand<span class="xml-tag"></title></span> |
| <span class="xml-tag"></book></span></pre></div><p class="paragraph"/>To write a custom marshaller you can do the following:<p class="paragraph"/><div class="code"><pre>class BookMarshaller <span class="java-keyword">implements</span> ObjectMarshaller<XML> {<p class="paragraph"/> <span class="java-keyword">public</span> <span class="java-object">boolean</span> supports(<span class="java-object">Object</span> object) { |
| <span class="java-keyword">return</span> object <span class="java-keyword">instanceof</span> Book |
| }<p class="paragraph"/> <span class="java-keyword">public</span> void marshalObject(<span class="java-object">Object</span> object, XML converter) { |
| Book book = (Book)object |
| converter.chars book.title |
| } |
| }</pre></div><p class="paragraph"/>And then register the marshaller with:<p class="paragraph"/><div class="code"><pre>XML.registerObjectMarshaller(<span class="java-keyword">new</span> BookMarshaller())</pre></div><p class="paragraph"/>With the custom <code>ObjectMarshaller</code> in place, the output is now:<p class="paragraph"/><div class="code"><pre><span class="xml-tag"><book></span>The Stand<span class="xml-tag"></book></span></pre></div><p class="paragraph"/><h4>Customizing the Name of the Root Element</h4><p class="paragraph"/>If you wish the customize the name of the surrounding element, you can implement <a href="https://grails.apache.org/docs/3.0.x/api/org/grails/web/converters/marshaller/NameAwareMarshaller.html" class="api">NameAwareMarshaller</a>:<p class="paragraph"/><div class="code"><pre>class BookMarshaller <span class="java-keyword">implements</span> ObjectMarshaller<XML>,NameAwareMarshaller {<p class="paragraph"/> ...<p class="paragraph"/> <span class="java-object">String</span> getElementName(<span class="java-object">Object</span> o) { |
| <span class="java-keyword">return</span> 'custom-book' |
| }<p class="paragraph"/>}</pre></div><p class="paragraph"/>With the above change the output would now be:<p class="paragraph"/><div class="code"><pre><span class="xml-tag"><custom-book></span>The Stand<span class="xml-tag"></custom-book></span></pre></div><p class="paragraph"/><h4>Outputting Markup Using the Converters API or Builder</h4><p class="paragraph"/>With the passed Converter object you can explicitly code to the Converters API to stream markup to the response:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">public</span> void marshalObject(<span class="java-object">Object</span> object, XML converter) { |
| Book book = (Book)object<p class="paragraph"/> converter.attribute 'id', book.id.toString() |
| converter.attribute 'date-released', book.dateReleased.toString()<p class="paragraph"/> converter.startNode 'title' |
| converter.chars book.title |
| converter.end()<p class="paragraph"/>}</pre></div><p class="paragraph"/>The above code results in:<p class="paragraph"/><div class="code"><pre><span class="xml-tag"><book id=<span class="xml-quote">"1"</span> date-released=<span class="xml-quote">"..."</span>></span> |
| <span class="xml-tag"><title></span>The Stand<span class="xml-tag"></title></span> |
| <span class="xml-tag"></book></span></pre></div><p class="paragraph"/>You can also use a builder notation to achieve a similar result (although the builder notation does not work for <code>CompileStatic</code>):<p class="paragraph"/><div class="code"><pre><span class="java-keyword">public</span> void marshalObject(<span class="java-object">Object</span> object, XML converter) { |
| Book b = (Book)object<p class="paragraph"/> converter.build { |
| book(id: b.id) { |
| title b.title |
| } |
| } |
| }</pre></div><p class="paragraph"/><h4>Using the convertAnother Method to Recursively Convert Objects</h4><p class="paragraph"/>To create more complex responses you can use the <code>convertAnother</code> method to convert associations and other objects:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">public</span> void marshalObject(<span class="java-object">Object</span> object, XML converter) { |
| Book book = (Book)object<p class="paragraph"/> converter.startNode 'title' |
| converter.chars book.title |
| converter.end()<p class="paragraph"/> <span class="java-keyword">if</span> (book.authors) { |
| converter.startNode 'authors' |
| <span class="java-keyword">for</span>(author in book.authors) { |
| converter.convertAnother author |
| } |
| converter.end() |
| } |
| }</pre></div> |
| |
| |
| |
| <h2 id="customRenderers">9.1.6.5 Implementing a Custom Renderer</h2> |
| If you want even more control of the rendering or prefer to use your own marshalling techniques then you can implement your own <code>Renderer</code> instance. For example below is a simple implementation that customizes the rendering of the <code>Book</code> class:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">package</span> myapp |
| <span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.render.* |
| <span class="java-keyword">import</span> grails.web.mime.MimeType<p class="paragraph"/>class BookXmlRenderer <span class="java-keyword">extends</span> AbstractRenderer<Book> { |
| BookXmlRenderer() { |
| <span class="java-keyword">super</span>(Book, [MimeType.XML,MimeType.TEXT_XML] as MimeType[]) |
| }<p class="paragraph"/> void render(Book object, RenderContext context) { |
| context.contentType = MimeType.XML.name<p class="paragraph"/> def xml = <span class="java-keyword">new</span> groovy.xml.MarkupBuilder(context.writer) |
| xml.book(id: object.id, title:object.title) |
| } |
| }</pre></div><p class="paragraph"/>The <code>AbstractRenderer</code> super class has a constructor that takes the class that it renders and the <code>MimeType</code>(s) that are accepted (via the ACCEPT header or file extension) for the renderer.<p class="paragraph"/>To configure this renderer, simply add it is a bean to <code>grails-app/conf/spring/resources.groovy</code>:<p class="paragraph"/><div class="code"><pre>beans = { |
| bookRenderer(myapp.BookXmlRenderer) |
| }</pre></div><p class="paragraph"/>The result will be that all <code>Book</code> instances will be rendered in the following format:<p class="paragraph"/><div class="code"><pre><book id=<span class="java-quote">"1"</span> title=<span class="java-quote">"The Stand"</span>/></pre></div><p class="paragraph"/><blockquote class="note"> |
| Note that if you change the rendering to a completely different format like the above, then you also need to change the binding if you plan to support POST and PUT requests. Grails will not automatically know how to bind data from a custom XML format to a domain class otherwise. See the section on "Customizing Binding of Resources" for further information. |
| </blockquote><p class="paragraph"/><h4>Container Renderers</h4><p class="paragraph"/>A <code>grails.rest.render.ContainerRenderer</code> is a renderer that renders responses for containers of objects (lists, maps, collections etc.). The interface is largely the same as the <code>Renderer</code> interface except for the addition of the <code>getComponentType()</code> method, which should return the "contained" type. For example:<p class="paragraph"/><div class="code"><pre>class BookListRenderer <span class="java-keyword">implements</span> ContainerRenderer<List, Book> { |
| <span class="java-object">Class</span><List> getTargetType() { List } |
| <span class="java-object">Class</span><Book> getComponentType() { Book } |
| MimeType[] getMimeTypes() { [ MimeType.XML] as MimeType[] } |
| void render(List object, RenderContext context) { |
| .... |
| } |
| }</pre></div> |
| |
| |
| |
| <h2 id="gspRenderers">9.1.6.6 Using GSP to Customize Rendering</h2> |
| You can also customize rendering on a per action basis using Groovy Server Pages (GSP). For example given the <code>show</code> action mentioned previously:<p class="paragraph"/><div class="code"><pre>def show(Book book) { |
| respond book |
| }</pre></div><p class="paragraph"/>You could supply a <code>show.xml.gsp</code> file to customize the rendering of the XML:<p class="paragraph"/><div class="code"><pre><%@page contentType=<span class="java-quote">"application/xml"</span>%> |
| <book id=<span class="java-quote">"${book.id}"</span> title=<span class="java-quote">"${book.title}"</span>/></pre></div><p class="paragraph"/> |
| |
| |
| <h2 id="hypermedia">9.1.7 Hypermedia as the Engine of Application State</h2> |
| <a href="http://en.wikipedia.org/wiki/HATEOAS" target="blank">HATEOAS</a>, an abbreviation for Hypermedia as the Engine of Application State, is a common pattern applied to REST architectures that uses hypermedia and linking to define the REST API.<p class="paragraph"/>Hypermedia (also called Mime or Media Types) are used to describe the state of a REST resource, and links tell clients how to transition to the next state. The format of the response is typically JSON or XML, although standard formats such as <a href="http://tools.ietf.org/html/rfc4287" target="blank">Atom</a> and/or <a href="http://stateless.co/hal_specification.html" target="blank">HAL</a> are frequently used. |
| |
| |
| |
| <h2 id="hal">9.1.7.1 HAL Support</h2> |
| <a href="http://stateless.co/hal_specification.html" target="blank">HAL</a> is a standard exchange format commonly used when developing REST APIs that follow HATEOAS principals. An example HAL document representing a list of orders can be seen below:<p class="paragraph"/><div class="code"><pre>{ |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { <span class="java-quote">"href"</span>: <span class="java-quote">"/orders"</span> }, |
| <span class="java-quote">"next"</span>: { <span class="java-quote">"href"</span>: <span class="java-quote">"/orders?page=2"</span> }, |
| <span class="java-quote">"find"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"/orders{?id}"</span>, |
| <span class="java-quote">"templated"</span>: <span class="java-keyword">true</span> |
| }, |
| <span class="java-quote">"admin"</span>: [{ |
| <span class="java-quote">"href"</span>: <span class="java-quote">"/admins/2"</span>, |
| <span class="java-quote">"title"</span>: <span class="java-quote">"Fred"</span> |
| }, { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"/admins/5"</span>, |
| <span class="java-quote">"title"</span>: <span class="java-quote">"Kate"</span> |
| }] |
| }, |
| <span class="java-quote">"currentlyProcessing"</span>: 14, |
| <span class="java-quote">"shippedToday"</span>: 20, |
| <span class="java-quote">"_embedded"</span>: { |
| <span class="java-quote">"order"</span>: [{ |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { <span class="java-quote">"href"</span>: <span class="java-quote">"/orders/123"</span> }, |
| <span class="java-quote">"basket"</span>: { <span class="java-quote">"href"</span>: <span class="java-quote">"/baskets/98712"</span> }, |
| <span class="java-quote">"customer"</span>: { <span class="java-quote">"href"</span>: <span class="java-quote">"/customers/7809"</span> } |
| }, |
| <span class="java-quote">"total"</span>: 30.00, |
| <span class="java-quote">"currency"</span>: <span class="java-quote">"USD"</span>, |
| <span class="java-quote">"status"</span>: <span class="java-quote">"shipped"</span> |
| }, { |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { <span class="java-quote">"href"</span>: <span class="java-quote">"/orders/124"</span> }, |
| <span class="java-quote">"basket"</span>: { <span class="java-quote">"href"</span>: <span class="java-quote">"/baskets/97213"</span> }, |
| <span class="java-quote">"customer"</span>: { <span class="java-quote">"href"</span>: <span class="java-quote">"/customers/12369"</span> } |
| }, |
| <span class="java-quote">"total"</span>: 20.00, |
| <span class="java-quote">"currency"</span>: <span class="java-quote">"USD"</span>, |
| <span class="java-quote">"status"</span>: <span class="java-quote">"processing"</span> |
| }] |
| } |
| }</pre></div><p class="paragraph"/><h4>Exposing Resources Using HAL</h4><p class="paragraph"/>To return HAL instead of regular JSON for a resource you can simply override the renderer in <code>grails-app/conf/spring/resources.groovy</code> with an instance of <code>grails.rest.render.hal.HalJsonRenderer</code> (or <code>HalXmlRenderer</code> for the XML variation):<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.render.hal.* |
| beans = { |
| halBookRenderer(HalJsonRenderer, <span class="java-keyword">rest</span>.test.Book) |
| }</pre></div><p class="paragraph"/>With the bean in place requesting the HAL content type will return HAL:<p class="paragraph"/><div class="code"><pre>$ curl -i -H <span class="java-quote">"Accept: application/hal+json"</span> http://localhost:8080/myapp/books/1<p class="paragraph"/>HTTP/1.1 200 OK |
| Server: Apache-Coyote/1.1 |
| Content-Type: application/hal+json;charset=ISO-8859-1<p class="paragraph"/>{ |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books/1"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/hal+json"</span> |
| } |
| }, |
| <span class="java-quote">"title"</span>: <span class="java-quote">""The Stand""</span> |
| }</pre></div><p class="paragraph"/>To use HAL XML format simply change the renderer:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.render.hal.* |
| beans = { |
| halBookRenderer(HalXmlRenderer, <span class="java-keyword">rest</span>.test.Book) |
| }</pre></div><p class="paragraph"/><h4>Rendering Collections Using HAL</h4><p class="paragraph"/>To return HAL instead of regular JSON for a list of resources you can simply override the renderer in <code>grails-app/conf/spring/resources.groovy</code> with an instance of <code>grails.rest.render.hal.HalJsonCollectionRenderer</code>:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.render.hal.* |
| beans = { |
| halBookCollectionRenderer(HalJsonCollectionRenderer, <span class="java-keyword">rest</span>.test.Book) |
| }</pre></div><p class="paragraph"/>With the bean in place requesting the HAL content type will return HAL:<p class="paragraph"/><div class="code"><pre>$ curl -i -H <span class="java-quote">"Accept: application/hal+json"</span> http://localhost:8080/myapp/books |
| HTTP/1.1 200 OK |
| Server: Apache-Coyote/1.1 |
| Content-Type: application/hal+json;charset=UTF-8 |
| Transfer-Encoding: chunked |
| Date: Thu, 17 Oct 2013 02:34:14 GMT<p class="paragraph"/>{ |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/hal+json"</span> |
| } |
| }, |
| <span class="java-quote">"_embedded"</span>: { |
| <span class="java-quote">"book"</span>: [ |
| { |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books/1"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/hal+json"</span> |
| } |
| }, |
| <span class="java-quote">"title"</span>: <span class="java-quote">"The Stand"</span> |
| }, |
| { |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books/2"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/hal+json"</span> |
| } |
| }, |
| <span class="java-quote">"title"</span>: <span class="java-quote">"Infinite Jest"</span> |
| }, |
| { |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books/3"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/hal+json"</span> |
| } |
| }, |
| <span class="java-quote">"title"</span>: <span class="java-quote">"Walden"</span> |
| } |
| ] |
| } |
| }</pre></div><p class="paragraph"/>Notice that the key associated with the list of <code>Book</code> objects in the rendered JSON is <code>book</code> which is derived from the type of objects in the collection, namely <code>Book</code>. In order to customize the value of this key assign a value to the <code>collectionName</code> property on the <code>HalJsonCollectionRenderer</code> bean as shown below:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.render.hal.* |
| beans = { |
| halBookCollectionRenderer(HalCollectionJsonRenderer, <span class="java-keyword">rest</span>.test.Book) { |
| collectionName = 'publications' |
| } |
| }</pre></div><p class="paragraph"/>With that in place the rendered HAL will look like the following:<p class="paragraph"/><div class="code"><pre>$ curl -i -H <span class="java-quote">"Accept: application/hal+json"</span> http://localhost:8080/myapp/books |
| HTTP/1.1 200 OK |
| Server: Apache-Coyote/1.1 |
| Content-Type: application/hal+json;charset=UTF-8 |
| Transfer-Encoding: chunked |
| Date: Thu, 17 Oct 2013 02:34:14 GMT<p class="paragraph"/>{ |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/hal+json"</span> |
| } |
| }, |
| <span class="java-quote">"_embedded"</span>: { |
| <span class="java-quote">"publications"</span>: [ |
| { |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books/1"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/hal+json"</span> |
| } |
| }, |
| <span class="java-quote">"title"</span>: <span class="java-quote">"The Stand"</span> |
| }, |
| { |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books/2"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/hal+json"</span> |
| } |
| }, |
| <span class="java-quote">"title"</span>: <span class="java-quote">"Infinite Jest"</span> |
| }, |
| { |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books/3"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/hal+json"</span> |
| } |
| }, |
| <span class="java-quote">"title"</span>: <span class="java-quote">"Walden"</span> |
| } |
| ] |
| } |
| }</pre></div><p class="paragraph"/><h4>Using Custom Media / Mime Types</h4><p class="paragraph"/>If you wish to use a custom Mime Type then you first need to declare the Mime Types in <code>grails-app/conf/application.groovy</code>:<p class="paragraph"/><div class="code"><pre>grails.mime.types = [ |
| all: <span class="java-quote">"*/*"</span>, |
| book: <span class="java-quote">"application/vnd.books.org.book+json"</span>, |
| bookList: <span class="java-quote">"application/vnd.books.org.booklist+json"</span>, |
| … |
| ]</pre></div><p class="paragraph"/><blockquote class="warning"> |
| It is critical that place your new mime types after the 'all' Mime Type because if the Content Type of the request cannot be established then the first entry in the map is used for the response. If you have your new Mime Type at the top then Grails will always try and send back your new Mime Type if the requested Mime Type cannot be established. |
| </blockquote><p class="paragraph"/>Then override the renderer to return HAL using the custom Mime Types:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.render.hal.* |
| <span class="java-keyword">import</span> grails.web.mime.*<p class="paragraph"/>beans = { |
| halBookRenderer(HalJsonRenderer, <span class="java-keyword">rest</span>.test.Book, <span class="java-keyword">new</span> MimeType(<span class="java-quote">"application/vnd.books.org.book+json"</span>, [v:<span class="java-quote">"1.0"</span>])) |
| halBookListRenderer(HalJsonCollectionRenderer, <span class="java-keyword">rest</span>.test.Book, <span class="java-keyword">new</span> MimeType(<span class="java-quote">"application/vnd.books.org.booklist+json"</span>, [v:<span class="java-quote">"1.0"</span>])) |
| }</pre></div><p class="paragraph"/>In the above example the first bean defines a HAL renderer for a single book instance that returns a Mime Type of <code>application/vnd.books.org.book+json</code>. The second bean defines the Mime Type used to render a collection of books (in this case <code>application/vnd.books.org.booklist+json</code>).<p class="paragraph"/><blockquote class="note"> |
| <code>application/vnd.books.org.booklist+json</code> is an example of a media-range (http://www.w3.org/Protocols/rfc2616/rfc2616.html - Header Field Definitions). This example uses entity (book) and operation (list) to form the media-range values but in reality, it may not be necessary to create a separate Mime type for each operation. Further, it may not be necessary to create Mime types at the entity level. See the section on "Versioning REST resources" for further information about how to define your own Mime types. |
| </blockquote><p class="paragraph"/>With this in place issuing a request for the new Mime Type returns the necessary HAL:<p class="paragraph"/><div class="code"><pre>$ curl -i -H <span class="java-quote">"Accept: application/vnd.books.org.book+json"</span> http://localhost:8080/myapp/books/1<p class="paragraph"/>HTTP/1.1 200 OK |
| Server: Apache-Coyote/1.1 |
| Content-Type: application/vnd.books.org.book+json;charset=ISO-8859-1<p class="paragraph"/> |
| { |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books/1"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/vnd.books.org.book+json"</span> |
| } |
| }, |
| <span class="java-quote">"title"</span>: <span class="java-quote">""The Stand""</span> |
| }</pre></div><p class="paragraph"/> |
| <h4>Customizing Link Rendering</h4><p class="paragraph"/>An important aspect of HATEOAS is the usage of links that describe the transitions the client can use to interact with the REST API. By default the <code>HalJsonRenderer</code> will automatically create links for you for associations and to the resource itself (using the "self" relationship).<p class="paragraph"/>However you can customize link rendering using the <code>link</code> method that is added to all domain classes annotated with <code>grails.rest.Resource</code> or any class annotated with <code>grails.rest.Linkable</code>. For example, the <code>show</code> action can be modified as follows to provide a new link in the resulting output:<p class="paragraph"/><div class="code"><pre>def show(Book book) { |
| book.link rel:'publisher', href: g.createLink(absolute: <span class="java-keyword">true</span>, resource:<span class="java-quote">"publisher"</span>, params:[bookId: book.id]) |
| respond book |
| }</pre></div><p class="paragraph"/>Which will result in output such as:<p class="paragraph"/><div class="code"><pre>{ |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"self"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books/1"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span>, |
| <span class="java-quote">"type"</span>: <span class="java-quote">"application/vnd.books.org.book+json"</span> |
| } |
| <span class="java-quote">"publisher"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/myapp/books/1/publisher"</span>, |
| <span class="java-quote">"hreflang"</span>: <span class="java-quote">"en"</span> |
| } |
| }, |
| <span class="java-quote">"title"</span>: <span class="java-quote">""The Stand""</span> |
| }</pre></div><p class="paragraph"/>The <code>link</code> method can be passed named arguments that match the properties of the <code>grails.rest.Link</code> class. |
| |
| |
| |
| <h2 id="atom">9.1.7.2 Atom Support</h2> |
| <a href="http://tools.ietf.org/html/rfc4287" target="blank">Atom</a> is another standard interchange format used to implement REST APIs. An example of Atom output can be seen below:<p class="paragraph"/><div class="code"><pre><span class="xml-tag"><?xml version=<span class="xml-quote">"1.0"</span> encoding=<span class="xml-quote">"utf-8"</span>?></span> |
| <span class="xml-tag"><feed xmlns=<span class="xml-quote">"http://www.w3.org/2005/Atom"</span>></span><p class="paragraph"/> <span class="xml-tag"><title></span>Example Feed<span class="xml-tag"></title></span> |
| <span class="xml-tag"><link href=<span class="xml-quote">"http://example.org/"</span>/></span> |
| <span class="xml-tag"><updated></span>2003-12-13T18:30:02Z<span class="xml-tag"></updated></span> |
| <span class="xml-tag"><author></span> |
| <span class="xml-tag"><name></span>John Doe<span class="xml-tag"></name></span> |
| <span class="xml-tag"></author></span> |
| <span class="xml-tag"><id></span>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6<span class="xml-tag"></id></span><p class="paragraph"/> <span class="xml-tag"><entry></span> |
| <span class="xml-tag"><title></span>Atom-Powered Robots Run Amok<span class="xml-tag"></title></span> |
| <span class="xml-tag"><link href=<span class="xml-quote">"http://example.org/2003/12/13/atom03"</span>/></span> |
| <span class="xml-tag"><id></span>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a<span class="xml-tag"></id></span> |
| <span class="xml-tag"><updated></span>2003-12-13T18:30:02Z<span class="xml-tag"></updated></span> |
| <span class="xml-tag"><summary></span>Some text.<span class="xml-tag"></summary></span> |
| <span class="xml-tag"></entry></span><p class="paragraph"/><span class="xml-tag"></feed></span></pre></div><p class="paragraph"/>To use Atom rendering again simply define a custom renderer:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">import</span> grails.<span class="java-keyword">rest</span>.render.atom.* |
| beans = { |
| halBookRenderer(AtomRenderer, <span class="java-keyword">rest</span>.test.Book) |
| halBookListRenderer(AtomCollectionRenderer, <span class="java-keyword">rest</span>.test.Book) |
| }</pre></div> |
| |
| |
| <h2 id="vndError">9.1.7.3 Vnd.Error Support</h2> |
| <a href="https://github.com/blongden/vnd.error" target="blank">Vnd.Error</a> is a standardised way of expressing an error response.<p class="paragraph"/>By default when a validation error occurs when attempting to POST new resources then the errors object will be sent back allow with a 422 respond code:<p class="paragraph"/><div class="code"><pre>$ curl -i -H <span class="java-quote">"Accept: application/json"</span> -H <span class="java-quote">"Content-Type: application/json"</span> -X POST -d <span class="java-quote">""</span> http://localhost:8080/myapp/books<p class="paragraph"/>HTTP/1.1 422 Unprocessable Entity |
| Server: Apache-Coyote/1.1 |
| Content-Type: application/json;charset=ISO-8859-1<p class="paragraph"/>{<span class="java-quote">"errors"</span>:[{<span class="java-quote">"object"</span>:<span class="java-quote">"<span class="java-keyword">rest</span>.test.Book"</span>, <span class="java-quote">"field"</span>:<span class="java-quote">"title"</span>, <span class="java-quote">"rejected-value"</span>:<span class="java-keyword">null</span>, <span class="java-quote">"message"</span>:<span class="java-quote">"Property [title] of class [class <span class="java-keyword">rest</span>.test.Book] cannot be <span class="java-keyword">null</span>"</span>}]}</pre></div><p class="paragraph"/>If you wish to change the format to Vnd.Error then simply register <code>grails.rest.render.errors.VndErrorJsonRenderer</code> bean in <code>grails-app/conf/spring/resources.groovy</code>:<p class="paragraph"/> |
| <div class="code"><pre>beans = { |
| vndJsonErrorRenderer(grails.<span class="java-keyword">rest</span>.render.errors.VndErrorJsonRenderer) |
| // <span class="java-keyword">for</span> Vnd.Error XML format |
| vndXmlErrorRenderer(grails.<span class="java-keyword">rest</span>.render.errors.VndErrorXmlRenderer) |
| }</pre></div><p class="paragraph"/>Then if you alter the client request to accept Vnd.Error you get an appropriate response:<p class="paragraph"/><div class="code"><pre>$ curl -i -H <span class="java-quote">"Accept: application/vnd.error+json,application/json"</span> -H <span class="java-quote">"Content-Type: application/json"</span> -X POST -d <span class="java-quote">""</span> http://localhost:8080/myapp/books |
| HTTP/1.1 200 OK |
| Server: Apache-Coyote/1.1 |
| Content-Type: application/vnd.error+json;charset=ISO-8859-1<p class="paragraph"/>[ |
| { |
| <span class="java-quote">"logref"</span>: <span class="java-quote">""book.nullable""</span>, |
| <span class="java-quote">"message"</span>: <span class="java-quote">"Property [title] of class [class <span class="java-keyword">rest</span>.test.Book] cannot be <span class="java-keyword">null</span>"</span>, |
| <span class="java-quote">"_links"</span>: { |
| <span class="java-quote">"resource"</span>: { |
| <span class="java-quote">"href"</span>: <span class="java-quote">"http://localhost:8080/<span class="java-keyword">rest</span>-test/books"</span> |
| } |
| } |
| } |
| ]</pre></div> |
| |
| |
| |
| <h2 id="binding">9.1.8 Customizing Binding of Resources</h2> |
| The framework provides a sophisticated but simple mechanism for binding REST requests to domain objects and command objects. One way to take advantage of this is to bind the <code>request</code> property in a controller the <code>properties</code> of a domain class. Given the following XML as the body of the request, the <code>createBook</code> action will create a new <code>Book</code> and assign "The Stand" to the <code>title</code> property and "Stephen King" to the <code>authorName</code> property.<p class="paragraph"/><div class="code"><pre><?xml version=<span class="java-quote">"1.0"</span> encoding=<span class="java-quote">"UTF-8"</span>?> |
| <book> |
| <title>The Stand</title> |
| <authorName>Stephen King</authorName> |
| </book></pre></div><p class="paragraph"/><div class="code"><pre>class BookController {<p class="paragraph"/> def createBook() { |
| def book = <span class="java-keyword">new</span> Book() |
| book.properties = request<p class="paragraph"/> // … |
| } |
| }</pre></div><p class="paragraph"/>Command objects will automatically be bound with the body of the request:<p class="paragraph"/><div class="code"><pre>class BookController { |
| def createBook(BookCommand book) {<p class="paragraph"/> // … |
| } |
| }<p class="paragraph"/>class BookCommand { |
| <span class="java-object">String</span> title |
| <span class="java-object">String</span> authorName |
| }</pre></div><p class="paragraph"/>If the command object type is a domain class and the root element of the XML document contains an <code>id</code> attribute, the <code>id</code> value will be used to retrieve the corresponding persistent instance from the database and then the rest of the document will be bound to the instance. If no corresponding record is found in the database, the command object reference will be null.<p class="paragraph"/><div class="code"><pre><?xml version=<span class="java-quote">"1.0"</span> encoding=<span class="java-quote">"UTF-8"</span>?> |
| <book id=<span class="java-quote">"42"</span>> |
| <title>Walden</title> |
| <authorName>Henry David Thoreau</authorName> |
| </book></pre></div><p class="paragraph"/><div class="code"><pre>class BookController { |
| def updateBook(Book book) { |
| // The book will have been retrieved from the database and updated |
| // by doing something like <span class="java-keyword">this</span>: |
| // |
| // book == Book.get('42') |
| // <span class="java-keyword">if</span>(book != <span class="java-keyword">null</span>) { |
| // book.properties = request |
| // } |
| // |
| // the code above represents what the framework will |
| // have done. There is no need to write that code.<p class="paragraph"/> // ...<p class="paragraph"/> } |
| }</pre></div><p class="paragraph"/>The data binding depends on an instance of the <a href="https://grails.apache.org/docs/3.0.x/api/org/grails/databinding/DataBindingSource.html" class="api">DataBindingSource</a> interface created by an instance of the <a href="https://grails.apache.org/docs/3.0.x/api/org/grails/databinding/bindingsource/DataBindingSourceCreator.html" class="api">DataBindingSourceCreator</a> interface. The specific implementation of <code>DataBindingSourceCreator</code> will be selected based on the <code>contentType</code> of the request. Several implementations are provided to handle common content types. The default implementations will be fine for most use cases. The following table lists the content types which are supported by the core framework and which <code>DataBindingSourceCreator</code> implementations are used for each. All of the implementation classes are in the <code>org.grails.databinding.bindingsource</code> package.<p class="paragraph"/><table class="wiki-table" cellpadding="0" cellspacing="0" border="0"><tr><th>Content Type(s)</th><th>Bean Name</th><th>DataBindingSourceCreator Impl.</th></tr><tr class="table-odd"><td>application/xml, text/xml</td><td>xmlDataBindingSourceCreator</td><td>XmlDataBindingSourceCreator</td></tr><tr class="table-even"><td>application/json, text/json</td><td>jsonDataBindingSourceCreator</td><td>JsonDataBindingSourceCreator</td></tr><tr class="table-odd"><td>application/hal+json</td><td>halJsonDataBindingSourceCreator</td><td>HalJsonDataBindingSourceCreator</td></tr><tr class="table-even"><td>application/hal+xml</td><td>halXmlDataBindingSourceCreator</td><td>HalXmlDataBindingSourceCreator</td></tr></table><p class="paragraph"/>In order to provide your own <code>DataBindingSourceCreator</code> for any of those content types, write a class which implements |
| <code>DataBindingSourceCreator</code> and register an instance of that class in the Spring application context. If you |
| are replacing one of the existing helpers, use the corresponding bean name from above. If you are providing a |
| helper for a content type other than those accounted for by the core framework, the bean name may be anything that |
| you like but you should take care not to conflict with one of the bean names above.<p class="paragraph"/>The <code>DataBindingSourceCreator</code> interface defines just 2 methods:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">package</span> org.grails.databinding.bindingsource<p class="paragraph"/><span class="java-keyword">import</span> grails.web.mime.MimeType |
| <span class="java-keyword">import</span> grails.databinding.DataBindingSource<p class="paragraph"/>/** |
| * A factory <span class="java-keyword">for</span> DataBindingSource instances |
| * |
| * @since 2.3 |
| * @see DataBindingSourceRegistry |
| * @see DataBindingSource |
| * |
| */ |
| <span class="java-keyword">interface</span> DataBindingSourceCreator {<p class="paragraph"/> /** |
| * <code><span class="java-keyword">return</span> All of the {</code>link MimeType} supported by <span class="java-keyword">this</span> helper |
| */ |
| MimeType[] getMimeTypes()<p class="paragraph"/> /** |
| * Creates a DataBindingSource suitable <span class="java-keyword">for</span> binding bindingSource to bindingTarget |
| * |
| * @param mimeType a mime type |
| * @param bindingTarget the target of the data binding |
| * @param bindingSource the value being bound |
| * @<span class="java-keyword">return</span> a DataBindingSource |
| */ |
| DataBindingSource createDataBindingSource(MimeType mimeType, <span class="java-object">Object</span> bindingTarget, <span class="java-object">Object</span> bindingSource) |
| }</pre></div><p class="paragraph"/><a href="https://grails.apache.org/docs/3.0.x/api/org/grails/databinding/bindingsource/AbstractRequestbodyDataBindingSourceCreator.html" class="api">AbstractRequestBodyDataBindingSourceCreator</a> |
| is an abstract class designed to be extended to simplify writing custom <code>DataBindingSourceCreator</code> classes. Classes which |
| extend <code>AbstractRequestbodyDatabindingSourceCreator</code> need to implement a method named <code>createBindingSource</code> |
| which accepts an <code>InputStream</code> as an argument and returns a <code>DataBindingSource</code> as well as implementing the <code>getMimeTypes</code> |
| method described in the <code>DataBindingSourceCreator</code> interface above. The <code>InputStream</code> argument to <code>createBindingSource</code> |
| provides access to the body of the request.<p class="paragraph"/>The code below shows a simple implementation.<p class="paragraph"/> |
| <div class="code"><pre>// MyCustomDataBindingSourceCreator.groovy in |
| // src/groovy/com/demo/myapp/databinding |
| <span class="java-keyword">package</span> com.demo.myapp.databinding<p class="paragraph"/><span class="java-keyword">import</span> grails.web.mime.MimeType |
| <span class="java-keyword">import</span> grails.databinding.DataBindingSource |
| <span class="java-keyword">import</span> org...databinding.SimpleMapDataBindingSource |
| <span class="java-keyword">import</span> org...databinding.bindingsource.AbstractRequestBodyDataBindingSourceCreator<p class="paragraph"/>/** |
| * A custom DataBindingSourceCreator capable of parsing key value pairs out of |
| * a request body containing a comma separated list of key:value pairs like: |
| * |
| * name:Herman,age:99,town:STL |
| * |
| */ |
| class MyCustomDataBindingSourceCreator <span class="java-keyword">extends</span> AbstractRequestBodyDataBindingSourceCreator {<p class="paragraph"/> @Override |
| <span class="java-keyword">public</span> MimeType[] getMimeTypes() { |
| [<span class="java-keyword">new</span> MimeType('text/custom+demo+csv')] as MimeType[] |
| }<p class="paragraph"/> @Override |
| <span class="java-keyword">protected</span> DataBindingSource createBindingSource(InputStream inputStream) { |
| def map = [:]<p class="paragraph"/> def reader = <span class="java-keyword">new</span> InputStreamReader(inputStream)<p class="paragraph"/> // <span class="java-keyword">this</span> is an obviously naive parser and is intended |
| // <span class="java-keyword">for</span> demonstration purposes only.<p class="paragraph"/> reader.eachLine { line -> |
| def keyValuePairs = line.split(',') |
| keyValuePairs.each { keyValuePair -> |
| <span class="java-keyword">if</span>(keyValuePair?.trim()) { |
| def keyValuePieces = keyValuePair.split(':') |
| def key = keyValuePieces[0].trim() |
| def value = keyValuePieces[1].trim() |
| map[key] = value |
| } |
| } |
| }<p class="paragraph"/> // create and <span class="java-keyword">return</span> a DataBindingSource which contains the parsed data |
| <span class="java-keyword">new</span> SimpleMapDataBindingSource(map) |
| } |
| }</pre></div><p class="paragraph"/>An instance of <code>MyCustomDataSourceCreator</code> needs to be registered in the spring application context.<p class="paragraph"/><div class="code"><pre>// grails-app/conf/spring/resources.groovy |
| beans = {<p class="paragraph"/> myCustomCreator com.demo.myapp.databinding.MyCustomDataBindingSourceCreator<p class="paragraph"/> // … |
| }</pre></div><p class="paragraph"/> |
| With that in place the framework will use the <code>myCustomCreator</code> bean any time a <code>DataBindingSourceCreator</code> is needed |
| to deal with a request which has a <code>contentType</code> of "text/custom+demo+csv". |
| |
| |
| <a name="8.2 SOAP"><!-- Legacy link --></a> |
| <h2 id="SOAP">9.2 SOAP</h2> |
| Grails does not feature SOAP support out-of-the-box, but there are several plugins that can help for both producing SOAP servers and calling SOAP web services.<p class="paragraph"/><h4>SOAP Clients</h4><p class="paragraph"/>To call SOAP web services there are generally 2 approaches taken, one is to use a tool to generate client stubs, the other is to manually construct the SOAP calls. The former can be easier to use, but the latter provides more flexibility / control.<p class="paragraph"/>The <a href="http://grails.org/plugin/cxf-client" target="blank">CXF client plugin</a> uses the CXF framework, which includes a <code>wsdl2java</code> tool for generating a client. There is nothing Groovy/Grails specific here in the generated code as it simply provides a Java API which you can invoke to call SOAP web services.<p class="paragraph"/>See the documentation on the <a href="https://github.com/ctoestreich/cxf-client" target="blank">CXF client plugin</a> for further information.<p class="paragraph"/>Alternatively, if you prefer more control over your SOAP calls the <a href="https://github.com/jwagenleitner/groovy-wslite" target="blank">WS-Lite library</a> is an excellent choice and features a <a href="http://grails.org/plugin/wslite" target="blank">Grails plugin</a>. You have more control over the SOAP requests sent, and since Groovy has fantastic support for building and parsing XML it can be very productive approach.<p class="paragraph"/>Below is an example of a SOAP call with wslite:<p class="paragraph"/><div class="code"><pre>withSoap(serviceURL: 'http://www.holidaywebservice.com/Holidays/US/Dates/USHolidayDates.asmx') { |
| def response = send { |
| body { |
| GetMothersDay(xmlns: 'http://www.27seconds.com/Holidays/US/Dates/') { |
| year(2011) |
| } |
| } |
| } |
| println response.GetMothersDayResponse.GetMothersDayResult.text() |
| }</pre></div><p class="paragraph"/>It is not recommended that you use the <a href="http://groovy.codehaus.org/GroovyWS" target="blank">GroovyWS</a> library, it pulls in many dependencies which increases the likelihood of conflicts. The WSlite library provides a far simpler and easier to use solution.<p class="paragraph"/><h4>SOAP Servers</h4><p class="paragraph"/>Again, Grails does not have direct support for exposing SOAP web services, however if you wish to expose a SOAP service from your application then the <a href="http://grails.org/plugin/cxf" target="blank">CXF plugin</a> (not to be confused with the cxf-client plugin), provides an easy way to do so.<p class="paragraph"/>Typically it involves taking a Grails service and adding 'expose'-style configuration, such as the below:<p class="paragraph"/><div class="code"><pre><span class="java-keyword">static</span> expose = EndpointType.JAX_WS_WSDL |
| //your path (preferred) or url to wsdl |
| <span class="java-keyword">static</span> wsdl = 'org/grails/cxf/test/soap/CustomerService.wsdl'</pre></div><p class="paragraph"/>Please refer to the <a href="https://github.com/thorstadt/grails-cxf#soap" target="blank">documentation of the plugin</a> for more information. |
| |
| |
| <a name="8.3 RSS and Atom"><!-- Legacy link --></a> |
| <h2 id="RSSAndAtom">9.3 RSS and Atom</h2> |
| No direct support is provided for RSS or Atom within Grails. You could construct RSS or ATOM feeds with the <a href="../ref/Controllers/render.html" class="controllers">render</a> method's XML capability. There is however a <a href="http://grails.org/plugin/feeds" target="blank">Feeds plugin</a> available for Grails that provides a RSS and Atom builder using the popular <a href="https://rome.dev.java.net/" target="blank">ROME</a> library. An example of its usage can be seen below:<p class="paragraph"/><div class="code"><pre>def feed() { |
| render(feedType: <span class="java-quote">"rss"</span>, feedVersion: <span class="java-quote">"2.0"</span>) { |
| title = <span class="java-quote">"My test feed"</span> |
| link = <span class="java-quote">"http://your.test.server/yourController/feed"</span><p class="paragraph"/> <span class="java-keyword">for</span> (article in Article.list()) { |
| entry(article.title) { |
| link = <span class="java-quote">"http://your.test.server/article/${article.id}"</span> |
| article.content // <span class="java-keyword">return</span> the content |
| } |
| } |
| } |
| }</pre></div> |
| |
| |
| |
| <div style="clear:both;margin-top:15px;"></div> |
| |
| <div class="toc-item prev-left"><a href="../guide/traits.html"><< <strong>8</strong><span>Traits</span></a></div> |
| |
| <div class="toc-item next-right"><a href="../guide/async.html"><strong>10</strong><span>Asynchronous Programming</span> >></a></div> |
| |
| <div style="clear:both"></div> |
| </div> |
| </td> |
| <td id="col2"> |
| <div class="local clearfix"> |
| <div class="local-title"> |
| <a href="../guide/index.html" target="mainFrame">Quick Reference</a> |
| <span class="toggle">(<a href="#" onclick="localToggle(); return false;">hide</a>)</span> |
| </div> |
| <div class="menu"> |
| |
| <div class="menu-block"><h1 class="menu-title" onclick="toggleRef(this.parentNode.childNodes[1])">Command Line</h1><div class="menu-sub"> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/Usage.html">Usage</a></div> |
| |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/bug-report.html">bug-report</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/clean.html">clean</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/compile.html">compile</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/console.html">console</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-app.html">create-app</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-command.html">create-command</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-controller.html">create-controller</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-domain-class.html">create-domain-class</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-functional-test.html">create-functional-test</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-hibernate-cfg-xml.html">create-hibernate-cfg-xml</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-integration-test.html">create-integration-test</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-interceptor.html">create-interceptor</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-plugin.html">create-plugin</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-script.html">create-script</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-service.html">create-service</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-taglib.html">create-taglib</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/create-unit-test.html">create-unit-test</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/dependency-report.html">dependency-report</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/docs.html">docs</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/generate-all.html">generate-all</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/generate-controller.html">generate-controller</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/generate-views.html">generate-views</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/help.html">help</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/install-templates.html">install-templates</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/list-plugins.html">list-plugins</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/package-plugin.html">package-plugin</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/package.html">package</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/plugin-info.html">plugin-info</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/run-app.html">run-app</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/schema-export.html">schema-export</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/shell.html">shell</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/stats.html">stats</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/stop-app.html">stop-app</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/test-app.html">test-app</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Command%20Line/war.html">war</a> |
| </div> |
| |
| </div> |
| </div> |
| |
| <div class="menu-block"><h1 class="menu-title" onclick="toggleRef(this.parentNode.childNodes[1])">Constraints</h1><div class="menu-sub"> |
| |
| <div class="menu-item"><a href="../ref/Constraints/Usage.html">Usage</a></div> |
| |
| |
| <div class="menu-item"><a href="../ref/Constraints/attributes.html">attributes</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/bindable.html">bindable</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/blank.html">blank</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/creditCard.html">creditCard</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/email.html">email</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/inList.html">inList</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/matches.html">matches</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/max.html">max</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/maxSize.html">maxSize</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/min.html">min</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/minSize.html">minSize</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/notEqual.html">notEqual</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/nullable.html">nullable</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/range.html">range</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/scale.html">scale</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/size.html">size</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/unique.html">unique</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/url.html">url</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/validator.html">validator</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Constraints/widget.html">widget</a> |
| </div> |
| |
| </div> |
| </div> |
| |
| <div class="menu-block"><h1 class="menu-title" onclick="toggleRef(this.parentNode.childNodes[1])">Controllers</h1><div class="menu-sub"> |
| |
| <div class="menu-item"><a href="../ref/Controllers/Usage.html">Usage</a></div> |
| |
| |
| <div class="menu-item"><a href="../ref/Controllers/actionName.html">actionName</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/afterInterceptor.html">afterInterceptor</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/allowedMethods.html">allowedMethods</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/bindData.html">bindData</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/chain.html">chain</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/controllerName.html">controllerName</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/defaultAction.html">defaultAction</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/errors.html">errors</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/flash.html">flash</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/forward.html">forward</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/grailsApplication.html">grailsApplication</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/hasErrors.html">hasErrors</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/namespace.html">namespace</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/params.html">params</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/redirect.html">redirect</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/render.html">render</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/request.html">request</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/respond.html">respond</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/response.html">response</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/responseFormats.html">responseFormats</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/scope.html">scope</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/servletContext.html">servletContext</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/session.html">session</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/withForm.html">withForm</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Controllers/withFormat.html">withFormat</a> |
| </div> |
| |
| </div> |
| </div> |
| |
| <div class="menu-block"><h1 class="menu-title" onclick="toggleRef(this.parentNode.childNodes[1])">Database Mapping</h1><div class="menu-sub"> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/Usage.html">Usage</a></div> |
| |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/autoImport.html">autoImport</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/autoTimestamp.html">autoTimestamp</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/batchSize.html">batchSize</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/cache.html">cache</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/cascade.html">cascade</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/column.html">column</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/comment.html">comment</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/discriminator.html">discriminator</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/dynamicInsert.html">dynamicInsert</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/dynamicUpdate.html">dynamicUpdate</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/fetch.html">fetch</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/id.html">id</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/ignoreNotFound.html">ignoreNotFound</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/indexColumn.html">indexColumn</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/insertable.html">insertable</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/joinTable.html">joinTable</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/lazy.html">lazy</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/order.html">order</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/sort.html">sort</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/table.html">table</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/type.html">type</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/updateable.html">updateable</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Database%20Mapping/version.html">version</a> |
| </div> |
| |
| </div> |
| </div> |
| |
| <div class="menu-block"><h1 class="menu-title" onclick="toggleRef(this.parentNode.childNodes[1])">Domain Classes</h1><div class="menu-sub"> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/Usage.html">Usage</a></div> |
| |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/addTo.html">addTo</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/attach.html">attach</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/belongsTo.html">belongsTo</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/clearErrors.html">clearErrors</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/constraints.html">constraints</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/count.html">count</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/countBy.html">countBy</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/createCriteria.html">createCriteria</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/delete.html">delete</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/discard.html">discard</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/embedded.html">embedded</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/errors.html">errors</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/executeQuery.html">executeQuery</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/executeUpdate.html">executeUpdate</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/exists.html">exists</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/fetchMode.html">fetchMode</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/find.html">find</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/findAll.html">findAll</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/findAllBy.html">findAllBy</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/findAllWhere.html">findAllWhere</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/findBy.html">findBy</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/findOrCreateBy.html">findOrCreateBy</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/findOrCreateWhere.html">findOrCreateWhere</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/findOrSaveBy.html">findOrSaveBy</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/findOrSaveWhere.html">findOrSaveWhere</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/findWhere.html">findWhere</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/first.html">first</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/get.html">get</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/getAll.html">getAll</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/getDirtyPropertyNames.html">getDirtyPropertyNames</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/getPersistentValue.html">getPersistentValue</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/hasErrors.html">hasErrors</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/hasMany.html">hasMany</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/hasOne.html">hasOne</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/ident.html">ident</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/instanceOf.html">instanceOf</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/isAttached.html">isAttached</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/isDirty.html">isDirty</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/last.html">last</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/list.html">list</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/listOrderBy.html">listOrderBy</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/load.html">load</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/lock.html">lock</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/mapWith.html">mapWith</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/mappedBy.html">mappedBy</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/mapping.html">mapping</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/merge.html">merge</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/namedQueries.html">namedQueries</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/properties.html">properties</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/read.html">read</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/refresh.html">refresh</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/removeFrom.html">removeFrom</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/save.html">save</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/transients.html">transients</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/validate.html">validate</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/where.html">where</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/whereAny.html">whereAny</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/withCriteria.html">withCriteria</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/withNewSession.html">withNewSession</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/withSession.html">withSession</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Domain%20Classes/withTransaction.html">withTransaction</a> |
| </div> |
| |
| </div> |
| </div> |
| |
| <div class="menu-block"><h1 class="menu-title" onclick="toggleRef(this.parentNode.childNodes[1])">Plug-ins</h1><div class="menu-sub"> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/Usage.html">Usage</a></div> |
| |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/URL%20mappings.html">URL mappings</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/codecs.html">codecs</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/controllers.html">controllers</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/core.html">core</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/dataSource.html">dataSource</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/domainClasses.html">domainClasses</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/hibernate.html">hibernate</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/i18n.html">i18n</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/logging.html">logging</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/scaffolding.html">scaffolding</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/services.html">services</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Plug-ins/servlets.html">servlets</a> |
| </div> |
| |
| </div> |
| </div> |
| |
| <div class="menu-block"><h1 class="menu-title" onclick="toggleRef(this.parentNode.childNodes[1])">Services</h1><div class="menu-sub"> |
| |
| <div class="menu-item"><a href="../ref/Services/Usage.html">Usage</a></div> |
| |
| |
| <div class="menu-item"><a href="../ref/Services/scope.html">scope</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Services/transactional.html">transactional</a> |
| </div> |
| |
| </div> |
| </div> |
| |
| <div class="menu-block"><h1 class="menu-title" onclick="toggleRef(this.parentNode.childNodes[1])">Servlet API</h1><div class="menu-sub"> |
| |
| |
| <div class="menu-item"><a href="../ref/Servlet%20API/request.html">request</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Servlet%20API/response.html">response</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Servlet%20API/servletContext.html">servletContext</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Servlet%20API/session.html">session</a> |
| </div> |
| |
| </div> |
| </div> |
| |
| <div class="menu-block"><h1 class="menu-title" onclick="toggleRef(this.parentNode.childNodes[1])">Tag Libraries</h1><div class="menu-sub"> |
| |
| <div class="menu-item"><a href="../ref/Tag%20Libraries/Usage.html">Usage</a></div> |
| |
| |
| <div class="menu-item"><a href="../ref/Tag%20Libraries/actionName.html">actionName</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tag%20Libraries/controllerName.html">controllerName</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tag%20Libraries/flash.html">flash</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tag%20Libraries/pageScope.html">pageScope</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tag%20Libraries/params.html">params</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tag%20Libraries/request.html">request</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tag%20Libraries/response.html">response</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tag%20Libraries/servletContext.html">servletContext</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tag%20Libraries/session.html">session</a> |
| </div> |
| |
| </div> |
| </div> |
| |
| <div class="menu-block"><h1 class="menu-title" onclick="toggleRef(this.parentNode.childNodes[1])">Tags</h1><div class="menu-sub"> |
| |
| |
| <div class="menu-item"><a href="../ref/Tags/actionSubmit.html">actionSubmit</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/actionSubmitImage.html">actionSubmitImage</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/applyLayout.html">applyLayout</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/checkBox.html">checkBox</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/collect.html">collect</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/cookie.html">cookie</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/country.html">country</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/countrySelect.html">countrySelect</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/createLink.html">createLink</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/createLinkTo.html">createLinkTo</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/currencySelect.html">currencySelect</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/datePicker.html">datePicker</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/each.html">each</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/eachError.html">eachError</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/else.html">else</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/elseif.html">elseif</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/external.html">external</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/field.html">field</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/fieldError.html">fieldError</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/fieldValue.html">fieldValue</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/findAll.html">findAll</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/form.html">form</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/formatBoolean.html">formatBoolean</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/formatDate.html">formatDate</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/formatNumber.html">formatNumber</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/grep.html">grep</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/hasErrors.html">hasErrors</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/header.html">header</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/hiddenField.html">hiddenField</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/if.html">if</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/img.html">img</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/include.html">include</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/isAvailable.html">isAvailable</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/isNotAvailable.html">isNotAvailable</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/javascript.html">javascript</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/join.html">join</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/layoutBody.html">layoutBody</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/layoutHead.html">layoutHead</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/layoutTitle.html">layoutTitle</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/link.html">link</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/localeSelect.html">localeSelect</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/message.html">message</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/meta.html">meta</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/pageProperty.html">pageProperty</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/paginate.html">paginate</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/passwordField.html">passwordField</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/radio.html">radio</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/radioGroup.html">radioGroup</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/render.html">render</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/renderErrors.html">renderErrors</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/resource.html">resource</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/select.html">select</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/set.html">set</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/sortableColumn.html">sortableColumn</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/submitButton.html">submitButton</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/textArea.html">textArea</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/textField.html">textField</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/timeZoneSelect.html">timeZoneSelect</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/unless.html">unless</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/uploadForm.html">uploadForm</a> |
| </div> |
| |
| <div class="menu-item"><a href="../ref/Tags/while.html">while</a> |
| </div> |
| |
| </div> |
| </div> |
| |
| </div> |
| </div> |
| </td> |
| </tr> |
| </table> |
| |
| <div id="footer"> |
| Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. |
| |
| </div> |
| |
| <script type="text/javascript" src="../js/docs.js"></script> |
| |
| </body> |
| </html> |