5.218 Pre- and Post-Processing Hooks

MWS provides functionality to intercept and modify data sent to and returned from web services for all available resources. This is done by creating hooks in Groovy files located in a sub-directory of the MWS_HOME directory (by default, /opt/mws/hooks).

Please see Reference in this topic for the full reference for available hooks and methods available to them.

5.218.1 Configuring Hooks

The directory of the hooks folder may be changed by providing a value for mws.hooks.location in the configuration file. If the directory starts with a path separator (ie /path/to/hooks), it will be treated as an absolute path. Otherwise, it will be used relative to the location of the MWS home directory (for more information, see Configuring Moab Web Services).

For example, if the MWS home directory is set to /opt/mws, the hooks directory by default would be in /opt/mws/hooks. Changing the mws.hooks.location property to myhooks would result in the hooks directory being located at /opt/mws/myhooks. Due to the default location of the MWS home directory, the default directory of the hooks directory is /opt/mws/hooks.

On startup, if the hooks directory does not exist, it will be created with a simple README.txt file with instructions on how to create hooks, the objects available, and the hooks available. If the folder or file is unable to be created, a message will be printed on the log with the full location of a README file, copied into a temporary directory.

5.218.2 Defining Hooks for a Resource

Hooks are defined for resources by creating groovy class files in the hooks directory (MWS_HOME/hooks by default). Each groovy file must be named by the resource URL it is associated with and end in ".groovy". The following table shows some possible hook files that may be created. Notice that the virtual machines hook file is abbreviated as vms, just as the URL for virtual machines is /rest/vms. In most cases, the hook file names will exactly match the URLs. However, in cases of nested URLs—such as with "accounting/users"—the hook file name must replace slashes with periods. For example:

Resource Hook filename
Jobs jobs.groovy
Nodes nodes.groovy
Virtual Machines vms.groovy
Accounting Users accounting.users.groovy
Accounting Funds Reports Statement accounting.funds.reports.statement.groovy
Accounting Charge Rates accounting.charge-rates.groovy
url url.groovy

plugins.rm.groovy is a valid hook filename. It works for the following URL: /rest/plugins/<pluginID or all>/rm/<query or action> (for example, /rest/plugins/plugin1/rm/cluster-query).

A complete example of a hook file is as follows:

Complete Hook File
------------------------------------
					
// Example before hook
def beforeList = {
  // Perform actions here
  // Return true to allow the API call to execute normally
  return true
}

def beforeShow = {
  // Perform actions here
  // Render messages to the user with a 405 Method Not Allowed
  // HTTP response code
  renderMessages("Custom message here", 405)
  // Return false to stop normal execution of the API call
  return false
}

// Example after hook
def afterList = { o ->
  if (!isSuccess()) {
    // Handle error here
    return false
  }
  // Perform actions here
  return o
}

You must convert all actions or queries that are separated by dashes to a camel case. For example, the hooks called for "cluster-query" should be beforeClusterQuery and afterClusterQuery.

As the specific format for the hooks for before and after are different, each will be explained separately.

5.218.3 Before Hooks

As shown above, before hooks require no arguments. They can directly act on several properties, objects, and methods as described in Reference. The return value is one of the most important aspects of a before hook. If it is false, a renderMessages, renderObject, renderList, render, or redirect method must first be called. This signifies that the API call should be interrupted and the render or redirect action specified within the hook is to be completed immediately.

A return value of true signifies that the API call should continue normally. Parameters, session variables, request and response variables may all be modified within a before hook.

If no return value is explicitly given, the result of the last statement in the before hook to be executed will be returned. This may cause unexpected behavior if the last statement resolves to false.

For all methods available to before hooks as well as specific examples, see beforeSave.

5.218.4 After Hooks

After hooks are always passed one argument: the object or list that is to be rendered as JSON. This may be modified as desired, but note that the object or list value is either a JSONArray or JSONObject. Therefore, it may not be accessed and modified as a typical groovy Map.

Unlike before hooks, after hooks should not call the render* methods directly. This method will automatically be called on the resulting object or list returned. The redirect and render methods should also not be called at this point. Instead, if a custom object or list is desired to be used, the serializeObject and serializeList methods are available to create suitable results to return.

The return value of an after hook may be one of two possibilities:

The return value of the after hook, if not null or false, must be the modified object passed into the hook or an object or list created with the serialize* methods.

For all methods available to after hooks as well as specific examples, see afterSave.

5.218.5 Error Handling

After hooks, unlike the before hooks, have the possibility of handling errors encountered during the course of the request. Handling errors is as simple as adding a one-line check to the hook as shown above or in the following code:

if (!isSuccess()) {
  // Handle error
  return false
}

It is recommended that each after hook contain at least these lines of code to prevent confusion on what the input object or list represents or should look like.

The isSuccess() function is false if and only if the HTTP response code is 400 or higher, such as a 404 Not Found, 400 Bad Request, or 500 Internal Server Error and the cause of the error state was not in the associated before hook. In other words, objects and lists rendered in the before hook with any HTTP response code will never run the associated after hook.

When handling errors, the passed in object will always contain a messages property containing a list of strings describing the error(s) encountered.

5.218.6 Defining Common Hooks

Sometimes it is beneficial to create hooks which are executed for all calls of a certain type, such as a beforeList hook that is executed during the course of listing any resource. These are possible using an all.groovy file. The format of this file is exactly the same as other hook files. The order of execution is as follows:

  1. Before common hook executed.
  2. Before resource-specific hook executed.
  3. Normal API call executed.
  4. After resource-specific hook executed.
  5. After common hook executed.

5.218.7 Reference

This page gives specific examples and reference for implementing hooks in MWS.

Available hooks

The following table lists the available hooks for each resource with their associated HTTP method and description.

Name HTTP method Description
beforeList GET Runs before an API call that lists resources (for example, GET /rest/jobs).
afterList GET Runs after an API call that lists resources.
beforeShow GET Runs before an API call that returns a single resource (for example, GET /rest/jobs/job.1).
afterShow GET Runs after an API call that returns a single resource.
beforeSave POST Runs before an API call that saves a new resource (for example, POST /rest/jobs).
afterSave POST Runs after an API call that returns a single resource.
beforeUpdate PUT Runs before an API call that returns a single resource (for example, PUT /rest/jobs/job.1).
afterUpdate PUT Runs after an API call that returns a single resource.
beforeDelete DELETE Runs before an API call that returns a single resource (for example, DELETE /rest/jobs/job.1).
afterDelete DELETE Runs after an API call that returns a single resource.

If a resource does not support a certain operation, any hooks for that operation will simply be ignored—such as beforeSave and afterSave hooks for the Node resource, where saving is not supported.

Available properties

The following table lists the properties, objects, and methods available in all hooks. Note that although it is possible to directly call the render* methods in the after hooks, it is not recommended.

Name Type Description
params Map Contains all URL parameters as well as the body of the request as parsed JSON.
request HttpServletRequest Contains properties of the HTTP request.
response HttpServletResponse Contains properties of the HTTP response which can be modified directly.
session HttpSession Contains the session parameters which can be modified directly.
flash Map Temporary storage that stores objects within the session for the next request only.
controllerName String The name of the controller responding to the request. Only available in before hooks.
actionName String The name of the action to be run on the controller. Only available in before hooks.
apiVersion String The API version for the current request (for example, 1 for 7.0 and 7.1, 2 for 7.2).

The parsed JSON may be accessed in before hooks as a simple groovy Map with params[controllerName].

In addition, several methods are available to the hooks. These are described in the following sections.

Redirect

The redirect method may be used to redirect the request to another API call or an arbitrary URL.

redirect(uri:'/rest/jobs')	// uri is used for internal redirection within MWS
redirect(url:'http://adaptivecomputing.com')	// url is used for external redirection
redirect(uri:'http://adaptivecomputing.com', params:[lang:'en'])	// params may be used for URL parameters

The redirect method will use the GET HTTP method for the resulting redirected request.

See the redirect method's documentation for more information.

Rendering objects, lists, or messages

There are several render* methods available to handle any case where objects or lists are desired to be rendered directly from the hook without continuing to the API call. Three different methods may be used depending on the desired output object type:

Render object
------------------------------------

// Object that should be rendered as JSON
def objectToRender = …
// HTTP response code (bad request)
def responseCode = 400
// Render a simple object
renderObject(objectToRender)
// Render a simple object with a custom response code
renderObject(objectToRender, responseCode)
Render list
------------------------------------

// List that should be rendered as JSON
def listToRender = …
// If the totalCount property differs from resultCount, use this value instead
def totalCount = …
// HTTP response code (bad request)
def responseCode = 400
// Render a simple list
// 	Dynamically adds "resultCount" and "totalCount" properties based on the size of the input list
renderList(listToRender)
// Render a simple list with a custom "totalCount"
renderList(listToRender, totalCount)
// Render a simple list without changing the "totalCount" but with a custom response code
renderList(listToRender, null, responseCode)
// Render a simple list with a custom "totalCount" and response code
renderList(listToRender, totalCount, responseCode)
Render message(s)
------------------------------------

// Messages
def messageToRender = "Single message"
def messagesListToRender = ["Message 1", "Message 2"]
// HTTP response code (bad request)
def responseCode = 400
// Render messages as an object with a property of "messages" containing a list of the messages passed in
renderMessages(messageToRender)
renderMessages(messageToRender, responseCode)
// Supports either a single String or list of Strings
renderMessages(messagesListToRender)
renderMessages(messagesListToRender, responseCode)

It is not recommended to call any of these methods from an after hook.

Render

Less commonly used, the render method is also available directly. This may be used to render text directly, change the content-type of the output, and many other functions. See the render method's documentation for more information.

It is not recommended to call this method from an after hook.

Serialize objects

The serializeObject and serializeList methods may be used to convert a custom object or list respectively into a format usable for returning in the after hooks. Simply pass in the object or list and a serialized version will be returned from the method.

def afterShow = {
	def objectToRender = …
	def serializedObject = serializeObject(objectToRender)
	return serializedObject
}
def afterShow = {
	def listToRender = [...]
	def serializedList = serializeList(listToRender)
	return serializedList
}

Error handling

Error handling is only available in after hooks by using the following check:

if (!isSuccess()) {
	// Handle error
	return … // False or modified object/list to render
}

Usage examples

Override an API call

The following hook would serve to override an entire API call, the list call in this case, and return a messages list containing a single element of "Action is not supported" and a HTTP response code of 405 (Method Not Allowed):

def beforeList = {
	renderMessages("Action is not supported", 405)
	return false
}

To be even more specific and disallow the deletion of virtual machines, the following may be used as the vms.groovy file:

def beforeDelete = {
	renderMessages("Virtual Machine deletion is not allowed", 405)
	return false
}

Add an additional property during job creation

To add an additional property to a job definition during creation, create a beforeSave hook in the jobs.groovy file as follows:

def beforeSave = {
	// params[controllerName] is equivalent to params["job"] or params.job
	params[controllerName].user = "myuser"
}

This would cause the created job to have a user of myuser.

Redirect based on URL parameter

To redirect an API call if a certain URL parameter exists, create a beforeSave hook in the jobs.groovy file as follows:

def beforeSave = {
	if (params.external) {
		redirect(url:'http://example.com/create-job')
		return false;	// Stop API call
	}
}

This would cause an API call of PUT /rest/jobs?external=1 to redirect to GET http://example.com/create-job.

Remove a property from getting a single job

To remove a property from the output of getting a single job, create an afterShow hook in the jobs.groovy file as follows:

def afterShow = { o ->
	o.discard("group")
	return o
}

This will cause the resulting JSON to be missing the group property of the job resource. Note again that these calls must use the JSONArray and JSONObject classes as mentioned in After Hooks.

Filter list items

To filter the items in a list nodes request based on user provided query parameter in the URL, use the following in the nodes.groovy file. A sample request that would activate the filter is http://localhost:8080/mws/rest/nodes?api-version=3&filter-power=On.

def afterList = { o ->
	// Do not filter if the user did not ask for it
	if (!params['filter-power'])
		return o
	// o = {resultCount: x, totalCount: x, results:[...]}

	// Using a built-in groovy method findAll to return all
	//	list items that return true from the block
	def results = o.results.findAll { node ->
		// Includes the node only if the power equals the user input
		return params['filter-power'].equalsIgnoreCase(node.power)
	}

	// Sets the results on the return object and updates the counts
	o.element("results", results)
	o.element("resultCount", results.size())
	return o
}

To filter the items in a list nodes request based on values within the list itself, such as variable values, use the following in the nodes.groovy file.

def afterList = { o ->	
	// o = {resultCount: x, totalCount: x, results:[...]}
	// Using a built-in groovy method findAll to return all
	//	list items that return true from the block
	def results = o.results.findAll { node ->
		// Includes the node only if the variable "included" is set to "true"
		return node.variables?.included=="true"
	}

	// Sets the results on the return object and updates the counts
	o.element("results", results)
	o.element("resultCount", results.size())
	return o
}

Related Topics 

© 2016 Adaptive Computing