Skip to content

Commit

Permalink
REST: Support rendering the result of Foo.list() in HAL
Browse files Browse the repository at this point in the history
  • Loading branch information
graemerocher committed Jun 7, 2013
1 parent fc4b6ba commit 8e33e96
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 56 deletions.
Expand Up @@ -28,6 +28,10 @@ import org.springframework.http.HttpStatus
*/
public interface RenderContext {

/**
* @return The path the the resource
*/
String getResourcePath()
/**
* @return Returns the mime type accepted by the client or null if non specified
*/
Expand Down
Expand Up @@ -22,6 +22,7 @@ import grails.rest.Link
import grails.rest.Resource
import grails.rest.render.RenderContext
import grails.rest.render.Renderer
import grails.util.Environment
import groovy.transform.CompileStatic
import groovy.transform.TypeCheckingMode
import org.apache.commons.beanutils.PropertyUtils
Expand Down Expand Up @@ -65,7 +66,7 @@ class HalDomainClassJsonRenderer<T> implements Renderer<T> {

Gson gson = new Gson()
boolean absoluteLinks = true
boolean prettyPrint = false
boolean prettyPrint = Environment.isDevelopmentMode()
List<String> includes

@Autowired
Expand Down Expand Up @@ -106,28 +107,91 @@ class HalDomainClassJsonRenderer<T> implements Renderer<T> {

final clazz = object.class

if (DomainClassArtefactHandler.isDomainClass(clazz) && clazz.getAnnotation(Resource)) {
if (isDomainResource(clazz)) {
writeDomainWithEmbeddedAndLinks(clazz, object, writer, context.locale, mimeType, [] as Set)
} else if (object instanceof Collection) {
writer.beginObject()
.name(LINKS_ATTRIBUTE)
.beginObject()
final resourceRef = linkGenerator.link(uri:context.resourcePath, method: HttpMethod.GET.toString(), absolute: absoluteLinks)
final locale = context.locale
writeLink(SELF_ATTRIBUTE, getResourceTitle(resourceRef, locale),resourceRef, locale, mimeType ? mimeType.name : null, writer)
writer.endObject()
.name(EMBEDDED_ATTRIBUTE)
.beginArray()

for(o in ((Collection)object)) {
if (o && isDomainResource(o.getClass())) {
writeDomainWithEmbeddedAndLinks(o.class, o, writer, locale, mimeType, [] as Set)
}
}
writer.endArray()

}
}

protected boolean isDomainResource(Class clazz) {
DomainClassArtefactHandler.isDomainClass(clazz) && clazz.getAnnotation(Resource)
}

protected void writeDomainWithEmbeddedAndLinks(Class clazz, Object object, JsonWriter writer, Locale locale, MimeType contentType, Set writtenObjects) {

PersistentEntity entity = mappingContext.getPersistentEntity(clazz.name)
final metaClass = GroovySystem.metaClassRegistry.getMetaClass(entity.javaClass)
Map<Association, Object> associationMap = writeLinks(metaClass, object, entity, locale, contentType, writer)

writeDomain(metaClass, entity, object, writer)

if (associationMap) {
writer.name(EMBEDDED_ATTRIBUTE)
writer.beginObject()
for (entry in associationMap.entrySet()) {
final property = entry.key
writer.name(property.name)
final isSingleEnded = property instanceof ToOne
if (isSingleEnded) {
Object value = entry.value
if (writtenObjects.contains(value)) {
continue
}

if (value != null) {
final associatedEntity = property.associatedEntity
if (associatedEntity) {
writtenObjects << value
writeDomainWithEmbeddedAndLinks(associatedEntity.javaClass, value, writer, locale, null, writtenObjects)
}
}
} else {
final associatedEntity = property.associatedEntity
if(associatedEntity) {
writer.beginArray()
for (obj in entry.value) {
writtenObjects << obj
writeDomainWithEmbeddedAndLinks(associatedEntity.javaClass, obj, writer, locale,null, writtenObjects)
}
writer.endArray()
}
}

}
writer.endObject()
}
writer.endObject()
}

protected Map<Association, Object> writeLinks(MetaClass metaClass, object, PersistentEntity entity, Locale locale, MimeType contentType, JsonWriter writer) {
writer.beginObject()
writer.name(LINKS_ATTRIBUTE)
writer.beginObject()
final entityHref = linkGenerator.link(resource: object, method: HttpMethod.GET.toString(), absolute:absoluteLinks)
final entityHref = linkGenerator.link(resource: object, method: HttpMethod.GET.toString(), absolute: absoluteLinks)
final title = getLinkTitle(entity, locale)


writeLink(SELF_ATTRIBUTE, title, entityHref, locale, contentType ? contentType.name : null, writer)
writeLink(SELF_ATTRIBUTE, title, entityHref, locale, contentType ? contentType.name : null, writer)
if (object.respondsTo('links')) {
final extraLinks = getLinksForObject(object)
for(Link l in extraLinks) {
for (Link l in extraLinks) {
writeLink(l.rel, l.title, l.href, l.hreflang ?: locale, l.contentType, writer)
}
}
Expand All @@ -150,9 +214,9 @@ class HalDomainClassJsonRenderer<T> implements Renderer<T> {
// no links for embedded
associationMap[a] = value
} else if (value != null) {
final href = linkGenerator.link(resource: value, method: HttpMethod.GET, absolute:absoluteLinks)
final href = linkGenerator.link(resource: value, method: HttpMethod.GET, absolute: absoluteLinks)
final associationTitle = getLinkTitle(associatedEntity, locale)
writeLink(propertyName, associationTitle, href, locale, null,writer)
writeLink(propertyName, associationTitle, href, locale, null, writer)
associationMap[a] = value
}
} else if (!(a instanceof Basic)) {
Expand All @@ -163,53 +227,15 @@ class HalDomainClassJsonRenderer<T> implements Renderer<T> {
if (associatedEntity) {
final proxy = PropertyUtils.getProperty(object, propertyName)
final id = proxyHandler.getProxyIdentifier(proxy)
final href = linkGenerator.link(resource: associatedEntity.decapitalizedName, id: id, method: HttpMethod.GET, absolute:absoluteLinks)
final href = linkGenerator.link(resource: associatedEntity.decapitalizedName, id: id, method: HttpMethod.GET, absolute: absoluteLinks)
final associationTitle = getLinkTitle(associatedEntity, locale)
writeLink(propertyName, associationTitle, href, locale, null,writer)
}

}
}
writer.endObject()

writeDomain(metaClass, entity, object, writer)

if (associationMap) {
writer.name(EMBEDDED_ATTRIBUTE)
writer.beginObject()
for (entry in associationMap.entrySet()) {
final property = entry.key
writer.name(property.name)
final isSingleEnded = property instanceof ToOne
if (isSingleEnded) {
Object value = entry.value
if (writtenObjects.contains(value)) {
continue
}

if (value != null) {
final associatedEntity = property.associatedEntity
if (associatedEntity) {
writtenObjects << value
writeDomainWithEmbeddedAndLinks(associatedEntity.javaClass, value, writer, locale, null, writtenObjects)
}
}
} else {
final associatedEntity = property.associatedEntity
if(associatedEntity) {
writer.beginArray()
for (obj in entry.value) {
writtenObjects << obj
writeDomainWithEmbeddedAndLinks(associatedEntity.javaClass, obj, writer, locale,null, writtenObjects)
}
writer.endArray()
}
writeLink(propertyName, associationTitle, href, locale, null, writer)
}

}
writer.endObject()
}
writer.endObject()
associationMap
}

@CompileStatic(TypeCheckingMode.SKIP)
Expand All @@ -222,6 +248,13 @@ class HalDomainClassJsonRenderer<T> implements Renderer<T> {
messageSource.getMessage("resource.${propertyName}.href.title", [propertyName, entity.name] as Object[], "", locale)
}

protected String getResourceTitle(String uri, Locale locale) {
if (uri.startsWith('/')) uri = uri.substring(1)
if (uri.endsWith('/')) uri = uri.substring(0, uri.length()-1)
uri = uri.replace('/', '.')
messageSource.getMessage("resource.${uri}.href.title", [uri] as Object[], "", locale)
}

protected void writeLink(String rel, String title, String href, Locale locale, String type, JsonWriter writer) {
writer.name(rel)
.beginObject()
Expand Down
Expand Up @@ -23,6 +23,7 @@ import org.codehaus.groovy.grails.plugins.web.api.ResponseMimeTypesApi
import org.codehaus.groovy.grails.web.mime.MimeType
import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes
import org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest
import org.codehaus.groovy.grails.web.util.WebUtils
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.web.servlet.ModelAndView
Expand All @@ -39,6 +40,7 @@ class ServletRenderContext implements RenderContext{
GrailsWebRequest webRequest
Map internalModel
ResponseMimeTypesApi responseMimeTypesApi
private String resourcePath

ServletRenderContext(GrailsWebRequest webRequest) {
this(webRequest, null)
Expand All @@ -49,6 +51,18 @@ class ServletRenderContext implements RenderContext{
this.internalModel = internalModel
}

@Override
String getResourcePath() {
if (resourcePath == null) {
return WebUtils.getForwardURI(webRequest.request)
}
return resourcePath
}

void setResourcePath(String resourcePath) {
this.resourcePath = resourcePath
}

@Override
@CompileStatic(TypeCheckingMode.SKIP)
MimeType getAcceptMimeType() {
Expand Down
Expand Up @@ -17,6 +17,7 @@ import org.grails.plugins.web.rest.render.ServletRenderContext
import org.springframework.context.support.StaticMessageSource
import org.springframework.mock.web.MockServletContext
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.util.WebUtils
import spock.lang.Specification

/**
Expand All @@ -30,13 +31,7 @@ class HalDomainClassJsonRendererSpec extends Specification {
}
void "Test that the HAL renderer renders domain objects with appropriate links"() {
given:"A HAL renderer"
def renderer = new HalDomainClassJsonRenderer(Book)
renderer.mappingContext = mappingContext
renderer.messageSource = new StaticMessageSource()
renderer.linkGenerator = getLinkGenerator {
"/books"(resources:"book")
"/authors"(resources: "author")
}
HalDomainClassJsonRenderer renderer = getRenderer()

when:"A domain object is rendered"
def webRequest = GrailsWebUtil.bindMockWebRequest()
Expand All @@ -61,6 +56,45 @@ class HalDomainClassJsonRendererSpec extends Specification {

}

void "Test that the HAL renderer renders a list of domain objects with the appropriate links"() {
given:"A HAL renderer"
HalDomainClassJsonRenderer renderer = getRenderer()

when:"A domain object is rendered"
def webRequest = GrailsWebUtil.bindMockWebRequest()
def response = webRequest.response
def renderContext = new ServletRenderContext(webRequest)
final author = new Author(name: "Stephen King")
author.id = 2L
def book = new Book(title:"The Stand", author: author)
book.authors = []
book.authors << author
book.link(href:"/publisher", rel:"The Publisher")
final author2 = new Author(name: "King Stephen")
author2.id = 3L
book.authors << author2
book.id = 1L
webRequest.request.setAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE, "/authors")
renderer.render(book.authors, renderContext)
then:"The resulting HAL is correct"
response.contentType == HalDomainClassJsonRenderer.MIME_TYPE.name
response.contentAsString == '{"_links":{"self":{"href":"http://localhost/authors","hreflang":"en","type":"application/hal+json"}},"_embedded":[{"_links":{"self":{"href":"http://localhost/authors/2","hreflang":"en","type":"application/hal+json"}},"name":"\\"Stephen King\\""},{"_links":{"self":{"href":"http://localhost/authors/3","hreflang":"en","type":"application/hal+json"}},"name":"\\"King Stephen\\""}]'

}

protected HalDomainClassJsonRenderer getRenderer() {
def renderer = new HalDomainClassJsonRenderer(Book)
renderer.mappingContext = mappingContext
renderer.messageSource = new StaticMessageSource()
renderer.linkGenerator = getLinkGenerator {
"/books"(resources: "book")
"/authors"(resources: "author")
}
renderer
}



MappingContext getMappingContext() {
final context = new KeyValueMappingContext("")
context.addPersistentEntity(Book)
Expand Down
7 changes: 6 additions & 1 deletion grails-web/src/main/groovy/grails/util/GrailsWebUtil.java
Expand Up @@ -154,8 +154,13 @@ public static GrailsWebRequest bindMockWebRequest(WebApplicationContext ctx, Moc
public static GrailsWebRequest bindMockWebRequest() {
ServletContext servletContext = new MockServletContext();
MockHttpServletRequest request = new MockHttpServletRequest(servletContext);
MockHttpServletResponse response = new MockHttpServletResponse();
return bindMockWebRequest(servletContext, request, response);
}

private static GrailsWebRequest bindMockWebRequest(ServletContext servletContext, MockHttpServletRequest request, MockHttpServletResponse response) {
GrailsWebRequest webRequest = new GrailsWebRequest(request,
new MockHttpServletResponse(), servletContext);
response, servletContext);
request.setAttribute(GrailsApplicationAttributes.WEB_REQUEST, webRequest);
RequestContextHolder.setRequestAttributes(webRequest);
return webRequest;
Expand Down

0 comments on commit 8e33e96

Please sign in to comment.