(Quick Reference)

6 Creating custom mappers - Reference Documentation

Authors: Marc Palmer (marc@grailsrocks.com), Luke Daley (ld@ldaley.com), Peter N. Steinmetz (ndoc3@steinmetz.org)

Version: 1.2.14

6 Creating custom mappers

The resource processing chain uses ResourceMapper artefacts to do the work.

You can create your own ResourceMapper artefacts inside your application or plugins to perform tasks such as;

  • Moving and renaming resources
  • Changing the contents of resource files (compress, minify, compile etc)
  • Set HTTP response headers when requests are processed

Resource mappers only get the chance to map (process) each resource once - the final resource is then stored on disk to serve future requests.

6.1 Defining a mapper

Defining a resource mapper is easy. You create a file with ResourceMapper.groovy as the file suffix, in the grails-app/resourceMappers folder.

The name of the mapper is extracted from the filename like any other Grails artefact, for example "TestResourceMapper" yields a mapper with name "test".

Example grails-app/resourceMappers/TestResourceMapper.groovy:

import org.grails.plugin.resource.mapper.MapperPhase

class TestResourceMapper {

def phase = MapperPhase.MUTATION

def map(resource, config) { def file = new File(resource.processedFile.parentFile, "_${resource.processedFile.name}") assert resource.processedFile.renameTo(file) resource.processedFile = file resource.updateActualUrlFromProcessedFile() }

}

The only method a mapper must implement is map(resource, config) which is passed the ResourceMeta object that represents the resource and the mapper-specific config variables pulled out of grails.resources.mappers.<mappername>. NOTE: The name of the mapper is always in all lower case.

This method can do whatever it needs to the resource's file, provided it calls the updateActualUrlFromProcessedFile() method if the resource moves, unless you patch ResourceMeta.actualUrl manually.

You can change other properties of the resource, such as change the content type of the resource, add or modify tagAttributes (which are passed through when rendering the link for the resource).

That's all you need to do to create a mapper. The best way to learn how they work is to study the source of Cached-Resources and Zipped-Resources plugins.

6.2 Mapper phases and priority

The example mapper shows the "phase" property being set to MapperPhase.MUTATION. The MapperPhase enumeration provides the possible mapping phases in the order in which they occur during processing of resources:

enum MapperPhase {
    GENERATION, // create new assets = compile less files
    MUTATION, // alter/improve assets (may mean creating new/deleting aggregated resources) = spriting
    COMPRESSION, // reducing the file size but maintaining semantics = minify
    LINKNORMALISATION, // convert all inter asset references into a normal form = css links
    AGGREGATION, // combining mutiple assets into one = bundling
    RENAMING, // moving of physical assets = hashing
    LINKREALISATION, // convert normalised inter asset references into real form = css links
    ALTERNATEREPRESENTATION, // attach different representations of the asset = gzipping
    DISTRIBUTION, // moving assets to their hosting environment = s3, cdn
    ABSOLUTISATION, // update inter asset references to their distributed equivalent = css links
    NOTIFICATION // let the world know about the new resources = cache invalidation
}

In most cases it will be enough to specify your phase and operation (see next section). However in some cases there may be issues where multiple mappers in the same phase must operate in a specific order. In those cases a "priority" property can be set to specify the priority integer.

Mappers will be executed in phase order, in order of ascending priority within each phase. The default priority if not specified is equivalent to zero.

6.3 Operation

The optional "operation" property allows you to specify the name of the kind of work performed by the mapper. Users can then prevent any mappers of this operation from executing on their resources.

The common example is to specify exclude:"minify" in a resource declaration to prevent any kind of minifying mapper from being applied to a resource that is already minified.

A similar operation called e.g. "compress" could be used to prevent duplicate zipping of resources that may have been pre-compressed (such as images).

import org.grails.plugin.resource.mapper.MapperPhase

class TestResourceMapper {

def phase = MapperPhase.COMPRESS

def operation = "compression"

def map(resource, config) { // Zip the file here }

}

Note the operations and mapper names occupy the same namespace so that the "exclude" argument on resource declarations can apply to either.

Resources will fail fast at runtime if an operation is specified on any mapper, where there is also a mapper with a name the same as the operation.

6.4 Processing only the right types of files

Often a mapper is only meant to target certain file types or file patterns. Make it easier for your users by operating correctly out of the box by specifying defaultExcludes and/or defaultIncludes for your mapper, which will filter the resources passed to your mapper:

import org.grails.plugin.resource.mapper.MapperPhase

class TestResourceMapper {

def phase = MapperPhase.MUTATION

static defaultExcludes = [ '**/*.png', '**/*.gif', '**/*.jpg', '**/*.jpeg', '**/*.gz', '**/*.zip' ] static defaultIncludes = [ '/images/**' ]

def map(resource, config) { … }

These defaults can be overidden by the user with the grails.resources.<mappername>.includes and grails.resources.<mappername>.excludes Config variables.

6.5 Adding response headers and intercepting requests

Resource mappers also have the opportunity to take part in response handling so that they can adjust the response headers if necessary. You may need to adapt the handling of the current request to the request headers supplied.

Note that this cannot be used to change how the resource is mapped - the mapping is performed once only, but the response headers can be customized every time the file is requested.

As an example, the Zipped-Resources plugin uses this mechanism to set the Content-Encoding header:

import org.grails.plugin.resource.mapper.MapperPhase

class ZipResourceMapper {

static phase = MapperPhase.ALTERNATEREPRESENTATION

/** * Rename the file to a hash of it's contents, and set caching headers */ def map(resource, config) { // Do the zipping ...

// Set up response headers resource.requestProcessors << { req, resp -> resp.setHeader('Content-Encoding', 'gzip') } } }

Here the mapper adds a Closure that takes the request and response objects to the resource's requestProcessors list. These will be called in the order they are defined on the resource.