org.openmhealth.reference.servlet.Version1.java Source code

Java tutorial

Introduction

Here is the source code for org.openmhealth.reference.servlet.Version1.java

Source

/*******************************************************************************
 * Copyright 2013 Open mHealth
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package org.openmhealth.reference.servlet;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest;
import org.apache.oltu.oauth2.as.request.OAuthTokenRequest;
import org.apache.oltu.oauth2.as.response.OAuthASResponse;
import org.apache.oltu.oauth2.common.error.OAuthError.CodeResponse;
import org.apache.oltu.oauth2.common.error.OAuthError.TokenResponse;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.OAuthResponse;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import org.apache.oltu.oauth2.common.message.types.ResponseType;
import org.apache.oltu.oauth2.common.message.types.TokenType;
import org.openmhealth.reference.data.AuthorizationCodeBin;
import org.openmhealth.reference.data.AuthorizationCodeResponseBin;
import org.openmhealth.reference.data.AuthorizationTokenBin;
import org.openmhealth.reference.data.Registry;
import org.openmhealth.reference.data.ThirdPartyBin;
import org.openmhealth.reference.data.UserBin;
import org.openmhealth.reference.domain.AuthenticationToken;
import org.openmhealth.reference.domain.AuthorizationCode;
import org.openmhealth.reference.domain.AuthorizationCodeResponse;
import org.openmhealth.reference.domain.AuthorizationToken;
import org.openmhealth.reference.domain.Data;
import org.openmhealth.reference.domain.MultiValueResult;
import org.openmhealth.reference.domain.ThirdParty;
import org.openmhealth.reference.domain.User;
import org.openmhealth.reference.exception.OmhException;
import org.openmhealth.reference.filter.AuthFilter;
import org.openmhealth.reference.request.AuthenticationRequest;
import org.openmhealth.reference.request.DataReadRequest;
import org.openmhealth.reference.request.DataWriteRequest;
import org.openmhealth.reference.request.ListRequest;
import org.openmhealth.reference.request.OauthRegistrationRequest;
import org.openmhealth.reference.request.Request;
import org.openmhealth.reference.request.SchemaIdsRequest;
import org.openmhealth.reference.request.SchemaRequest;
import org.openmhealth.reference.request.SchemaVersionsRequest;
import org.openmhealth.reference.request.UserActivationRequest;
import org.openmhealth.reference.request.UserRegistrationRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * <p>
 * The controller for the version 1 of the Open mHealth API.
 * </p>
 * 
 * <p>
 * This class has no state and, therefore, is immutable.
 * </p>
 *
 * @author John Jenkins
 */
@Controller
@RequestMapping(Version1.PATH)
public class Version1 {
    /**
     * The root path for queries to this version of the API.
     */
    public static final String PATH = "/v1";

    /**
     * The path to the user registration end-point.
     */
    public static final String PATH_REGISTRATION = "/users/registration";
    /**
     * The path to the user activation end-point.
     */
    public static final String PATH_ACTIVATION = "/users/activation";

    /**
     * The username parameter for the authenticate requests.
     */
    public static final String PARAM_AUTHENTICATION_USERNAME = "username";
    /**
     * The password parameter for the authenticate requests.
     */
    public static final String PARAM_AUTHENTICATION_PASSWORD = "password";
    /**
     * The authentication token parameter for requests that require
     * authentication.
     */
    public static final String PARAM_AUTHENTICATION_AUTH_TOKEN = "omh_auth_token";
    /**
     * The authorization flag that indicates if the user granted the
     * third-party access.
     */
    public static final String PARAM_AUTHORIZATION_GRANTED = "granted";
    /**
     * The key for the code parameter when the user is responding to a request
     * to grant access to a third-party.
     */
    public static final String PARAM_AUTHORIZATION_CODE = "code";

    /**
     * The parameter for the number of records to skip in requests that use
     * paging.
     */
    public static final String PARAM_PAGING_NUM_TO_SKIP = "num_to_skip";
    /**
     * The parameter for the number of records to return in requests that use
     * paging.
     */
    public static final String PARAM_PAGING_NUM_TO_RETURN = "num_to_return";

    /**
     * The parameter for the unique identifier for a schema. This is sometimes
     * used as part of the URI for the RESTful implementation.
     */
    public static final String PARAM_SCHEMA_ID = "schema_id";
    /**
     * The parameter for the version of a schema. This is sometimes used as
     * part of the URI for the RESTful implementation.
     */
    public static final String PARAM_SCHEMA_VERSION = "schema_version";

    /**
     * A parameter that limits the results to only those that were created on
     * or after the given date.
     */
    public static final String PARAM_DATE_START = "t_start";
    /**
     * A parameter that limits the results to only those that were created on
     * or before the given date.
     */
    public static final String PARAM_DATE_END = "t_end";

    /**
     * The parameter that indicates to which user the data should pertain.
     */
    public static final String PARAM_OWNER = "owner";
    /**
     * The parameter that indicates that the data should be summarized, if
     * possible.
     */
    public static final String PARAM_SUMMARIZE = "summarize";
    /**
     * The parameter that indicates which columns of the data should be
     * returned.
     */
    public static final String PARAM_COLUMN_LIST = "column_list";

    /**
     * The parameter for the data when it is being uploaded.
     */
    public static final String PARAM_DATA = "data";

    /**
     * The header for the URL to the previous set of data for list requests.
     */
    public static final String HEADER_PREVIOUS = "Previous";
    /**
     * The header for the URL to the next set of data for list requests.
     */
    public static final String HEADER_NEXT = "Next";

    /**
     * The encoding for the previous and next URLs.
     */
    private static final String URL_ENCODING_UTF_8 = "UTF-8";

    /**
     * The logger for this class.
     */
    private static final Logger LOGGER = Logger.getLogger(Version1.class.getCanonicalName());

    /**
     * Creates an authentication request, authenticates the user and, if
     * successful, returns the user's credentials.
     * 
     * @param username
     *        The username of the user attempting to authenticate.
     * 
     * @param password
     *        The password of the user attempting to authenticate.
     * 
     * @param request
     *        The HTTP request object.
     * 
     * @param response
     *        The HTTP response object.
     * 
     * @return The authorization token.
     * 
     * @throws OmhException
     *         There was a problem with the request. This could be any of the
     *         sub-classes of {@link OmhException}.
     */
    @RequestMapping(value = "auth", method = RequestMethod.POST)
    public @ResponseBody String getAuthentication(
            @RequestParam(value = PARAM_AUTHENTICATION_USERNAME, required = true) final String username,
            @RequestParam(value = PARAM_AUTHENTICATION_PASSWORD, required = true) final String password,
            final HttpServletRequest request, final HttpServletResponse response) throws OmhException {

        // Create the authentication request from parameters.
        AuthenticationToken token = handleRequest(request, response, new AuthenticationRequest(username, password));

        // Add a cookie for the authentication token.
        Cookie cookie = new Cookie(PARAM_AUTHENTICATION_AUTH_TOKEN, token.getToken());
        // Set the expiration on the cookie.
        cookie.setMaxAge(new Long((token.getExpires() - System.currentTimeMillis()) / 1000).intValue());
        // Build the path without the "auth" part.
        String requestUri = request.getRequestURI();
        cookie.setPath(requestUri.substring(0, requestUri.length() - 5));
        // Make sure the cookie is only used with HTTPS.
        cookie.setSecure(true);
        // Add the cookie to the response.
        response.addCookie(cookie);

        // Return the token.
        return token.getToken();
    }

    /**
     * Creates a registration request for a third-party ("client" in OAuth
     * parlance).
     * 
     * @param name
     *        The third-party's name.
     * 
     * @param description
     *        The third-party's description.
     * 
     * @param redirectUri
     *        The location to redirect the user to after they have responded to
     *        an authorization request from this third-party.
     *        
     * @param request
     *        The HTTP request.
     * 
     * @param response
     *        The HTTP response.
     */
    @RequestMapping(value = "auth/oauth/registration", method = RequestMethod.POST)
    public @ResponseBody ThirdParty thirdPartyRegistration(
            @RequestParam(value = ThirdParty.JSON_KEY_NAME, required = true) final String name,
            @RequestParam(value = ThirdParty.JSON_KEY_DESCRIPTION, required = true) final String description,
            @RequestParam(value = ThirdParty.JSON_KEY_REDIRECT_URI, required = true) final String redirectUri,
            final HttpServletRequest request, final HttpServletResponse response) {

        // Make sure the authentication token was a parameter. This prevents
        // malicious code from "hijacking" the token by performing a POST and
        // having the browser inject it as only a cookie.
        if (!(Boolean) request.getAttribute(AuthFilter.ATTRIBUTE_AUTHENTICATION_TOKEN_IS_PARAM)) {

            throw new OmhException(
                    "To register a third-party, the authentication token is " + "required as a parameter.");
        }

        return handleRequest(request, response,
                new OauthRegistrationRequest(
                        (AuthenticationToken) request.getAttribute(AuthFilter.ATTRIBUTE_AUTHENTICATION_TOKEN), name,
                        description, redirectUri));
    }

    /**
     * <p>
     * The OAuth call where a user has been redirected to us by some
     * third-party in order for us to present them with an authorization
     * request, verify that the user is who they say they are, and grant or
     * deny the request.
     * </p>
     * 
     * <p>
     * This call will either redirect the user to the authorization HTML page
     * with the parameters embedded or it will return a non-2xx response with a
     * message indicating what was wrong with the request. Unfortunately,
     * because the problem with the request may be that the given client ID is
     * unknown, we have no way to direct the user back. If we simply force the
     * browser to "go back", it may result in an infinite loop where the
     * third-party continuously redirects them back to us and visa-versa. To
     * avoid this, we should simply return an error string and let the user
     * decide.
     * </p>
     * 
     * @param request
     *        The HTTP request.
     * 
     * @param response
     *        The HTTP response.
     * 
     * @return A OAuth-specified JSON response that indicates what was wrong
     *         with the request. If nothing was wrong with the request, a
     *         redirect would have been returned.
     * 
     * @throws IOException
     *         There was a problem responding to the client.
     * 
     * @throws OAuthSystemException
     *         The OAuth library encountered an error.
     */
    @RequestMapping(value = "auth/oauth/authorize", method = { RequestMethod.GET, RequestMethod.POST })
    public @ResponseBody String receiveAuthorizationCodeRequest(final HttpServletRequest request,
            final HttpServletResponse response) throws IOException, OAuthSystemException {

        // Create the OAuth request from the HTTP request.
        OAuthAuthzRequest oauthRequest;
        try {
            oauthRequest = new OAuthAuthzRequest(request);
        }
        // The request does not conform to the RFC, so we return a HTTP 400
        // with a reason.
        catch (OAuthProblemException e) {
            // Create the OAuth response.
            OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
                    .buildJSONMessage();

            // Set the status and return the error message.
            response.setStatus(oauthResponse.getResponseStatus());
            return oauthResponse.getBody();
        }

        // Validate that the user is requesting a "code" response type, which
        // is the only response type we accept.
        try {
            if (!ResponseType.CODE.toString().equals(oauthRequest.getResponseType())) {

                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(CodeResponse.UNSUPPORTED_RESPONSE_TYPE)
                        .setErrorDescription("The response type must be '" + ResponseType.CODE.toString()
                                + "' but was instead: " + oauthRequest.getResponseType())
                        .setState(oauthRequest.getState()).buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }
        } catch (IllegalArgumentException e) {
            // Create the OAuth response.
            OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(CodeResponse.UNSUPPORTED_RESPONSE_TYPE)
                    .setErrorDescription("The response type is unknown: " + oauthRequest.getResponseType())
                    .setState(oauthRequest.getState()).buildJSONMessage();

            // Set the status and return the error message.
            response.setStatus(oauthResponse.getResponseStatus());
            return oauthResponse.getBody();
        }

        // Make sure no redirect URI was given.
        if (oauthRequest.getRedirectURI() != null) {
            // Create the OAuth response.
            OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(CodeResponse.INVALID_REQUEST)
                    .setErrorDescription("A URI must not be given. Instead, the one given "
                            + "when the account was created will be used.")
                    .setState(oauthRequest.getState()).buildJSONMessage();

            // Set the status and return the error message.
            response.setStatus(oauthResponse.getResponseStatus());
            return oauthResponse.getBody();
        }

        // Attempt to get the third-party.
        ThirdParty thirdParty = ThirdPartyBin.getInstance().getThirdParty(oauthRequest.getClientId());
        // If the third-party is unknown, reject the request.
        if (thirdParty == null) {
            // Create the OAuth response.
            OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(CodeResponse.INVALID_REQUEST)
                    .setErrorDescription("The client ID is unknown: " + oauthRequest.getClientId())
                    .setState(oauthRequest.getState()).buildJSONMessage();

            // Set the status and return the error message.
            response.setStatus(oauthResponse.getResponseStatus());
            return oauthResponse.getBody();
        }

        // Attempt to get the scopes.
        Set<String> scopes = oauthRequest.getScopes();
        if ((scopes == null) || (scopes.size() == 0)) {
            // Create the OAuth response.
            OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(CodeResponse.INVALID_SCOPE).setErrorDescription("A scope is required.")
                    .setState(oauthRequest.getState()).buildJSONMessage();

            // Set the status and return the error message.
            response.setStatus(oauthResponse.getResponseStatus());
            return oauthResponse.getBody();
        }
        // Validate the scopes.
        Registry registry = Registry.getInstance();
        for (String scope : scopes) {
            if (registry.getSchemas(scope, null, 0, 1).size() != 1) {
                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(CodeResponse.INVALID_SCOPE)
                        .setErrorDescription("Each scope must be a known schema ID: " + scope)
                        .setState(oauthRequest.getState()).buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }
        }

        // Create the temporary code to be granted or rejected by the user.
        AuthorizationCode code = new AuthorizationCode(thirdParty, oauthRequest.getScopes(),
                oauthRequest.getState());

        // Store the authorization code.
        AuthorizationCodeBin.getInstance().storeCode(code);

        // Build the scope as specified by the OAuth specification.
        StringBuilder scopeBuilder = new StringBuilder();
        for (String scope : code.getScopes()) {
            // Add a space unless it's the first entity.
            if (scopeBuilder.length() != 0) {
                scopeBuilder.append(' ');
            }
            // Add the scope.
            scopeBuilder.append(scope);
        }

        // Set the redirect.
        response.sendRedirect(OAuthASResponse.authorizationResponse(request, HttpServletResponse.SC_FOUND)
                .setCode(code.getCode()).location("Authorize.html").setScope(scopeBuilder.toString())
                .setParam(ThirdParty.JSON_KEY_NAME, thirdParty.getName())
                .setParam(ThirdParty.JSON_KEY_DESCRIPTION, thirdParty.getDescription()).buildQueryMessage()
                .getLocationUri());
        // Since we are redirecting the user, we don't need to return anything.
        return null;
    }

    /**
     * <p>
     * Handles the response from the user regarding whether or not the user
     * granted permission to a third-party via OAuth. If the user's credentials
     * are invalid or there was a general error reading the request, an error
     * message will be returned and displayed to the user. Once we have the
     * third-party's information, we will do a best-effort to redirect the user
     * back to the third-party with a code, which the third-party can then use
     * to call us later to determine the actual failure.
     * </p>
     * 
     * @param username
     *        The user's username.
     * 
     * @param password
     *        The user's password.
     * 
     * @param granted
     *        Whether or not the permission was granted.
     * 
     * @param code
     *        The code that was created, but not yet validated by the user.
     * 
     * @param request
     *        The HTTP request object.
     * 
     * @param response
     *        The HTTP response object.
     */
    @RequestMapping(value = "auth/oauth/authorization", method = RequestMethod.POST)
    public void authenticateAuthorizationCodeRequest(
            @RequestParam(value = PARAM_AUTHENTICATION_USERNAME, required = true) final String username,
            @RequestParam(value = PARAM_AUTHENTICATION_PASSWORD, required = true) final String password,
            @RequestParam(value = PARAM_AUTHORIZATION_GRANTED, required = true) final boolean granted,
            @RequestParam(value = PARAM_AUTHORIZATION_CODE, required = false) final String code,
            final HttpServletRequest request, final HttpServletResponse response)
            throws IOException, OAuthSystemException {

        // Get the user. If the user's credentials are invalid for whatever
        // reason, an exception will be thrown and the page will echo back the
        // reason.
        User user = AuthenticationRequest.getUser(username, password);

        // Get the authorization code.
        AuthorizationCode authCode = AuthorizationCodeBin.getInstance().getCode(code);
        // If the code is unknown, we cannot redirect back to the third-party
        // because we don't know who they are.
        if (authCode == null) {
            throw new OmhException("The authorization code is unknown.");
        }

        // Verify that the code has not yet expired.
        if (System.currentTimeMillis() > authCode.getExpirationTime()) {
            response.sendRedirect(OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(CodeResponse.ACCESS_DENIED).setErrorDescription("The code has expired.")
                    .location(authCode.getThirdParty().getRedirectUri().toString()).setState(authCode.getState())
                    .buildQueryMessage().getLocationUri());
            return;
        }

        // Get the response if it already exists.
        AuthorizationCodeResponse codeResponse = AuthorizationCodeResponseBin.getInstance().getResponse(code);

        // If the response does not exist, attempt to create a new one and
        // save it.
        if (codeResponse == null) {
            // Create the new code.
            codeResponse = new AuthorizationCodeResponse(authCode, user, granted);

            // Store it.
            AuthorizationCodeResponseBin.getInstance().storeVerification(codeResponse);
        }
        // Make sure it is being verified by the same user.
        else if (!user.getUsername().equals(codeResponse.getOwner().getUsername())) {

            response.sendRedirect(OAuthASResponse.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
                    .setError(CodeResponse.ACCESS_DENIED)
                    .setErrorDescription("The code has already been verified by another " + "user.")
                    .location(authCode.getThirdParty().getRedirectUri().toString()).setState(authCode.getState())
                    .buildQueryMessage().getLocationUri());
        }
        // Make sure the same grant response is being made.
        else if (granted == codeResponse.getGranted()) {
            response.sendRedirect(OAuthASResponse.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
                    .setError(CodeResponse.ACCESS_DENIED)
                    .setErrorDescription("The user has re-submitted the same "
                            + "authorization code twice with competing " + "grant values.")
                    .location(authCode.getThirdParty().getRedirectUri().toString()).setState(authCode.getState())
                    .buildQueryMessage().getLocationUri());
        }
        // Otherwise, this is simply a repeat of the same request as before,
        // and we can simply ignore it.

        // Redirect the user back to the third-party with the authorization
        // code and state.
        response.sendRedirect(OAuthASResponse.authorizationResponse(request, HttpServletResponse.SC_OK)
                .location(authCode.getThirdParty().getRedirectUri().toString()).setCode(authCode.getCode())
                .setParam("state", authCode.getState()).buildQueryMessage().getLocationUri());
    }

    /**
     * <p>
     * The OAuth call when a third-party is attempting to exchange their
     * authorization request token for a valid authorization token. Because
     * this is a back-channel communication from the third-party, their ID and
     * secret must be given to authenticate them. They will then be returned
     * either an authorization token or an error message indicating what was
     * wrong with the request.
     * </p>
     * 
     * @param request
     *        The HTTP request object.
     * 
     * @param response
     *        The HTTP response object.
     * 
     * @return An OAuth-specified JSON error message or an OAuth-specified JSON
     *         response that includes the access and refresh tokens as well as
     *         the expiration and other information.
     * 
     * @throws OAuthSystemException
     *         The OAuth library encountered an error.
     */
    @RequestMapping(value = "auth/oauth/token", method = RequestMethod.POST)
    public @ResponseBody String createAuthorizationToken(final HttpServletRequest request,
            final HttpServletResponse response) throws OAuthSystemException, IOException {

        // Attempt to build an OAuth request from the HTTP request.
        OAuthTokenRequest oauthRequest;
        try {
            oauthRequest = new OAuthTokenRequest(request);
        }
        // If the HTTP request was not a valid OAuth token request, then we
        // have no other choice but to reject it as a bad request.
        catch (OAuthProblemException e) {
            // Build the OAuth response.
            OAuthResponse oauthResponse = OAuthResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
                    .buildJSONMessage();

            // Set the HTTP response status code from the OAuth response.
            response.setStatus(oauthResponse.getResponseStatus());

            // Return the error message.
            return oauthResponse.getBody();
        }

        // Attempt to get the client.
        ThirdParty thirdParty = ThirdPartyBin.getInstance().getThirdParty(oauthRequest.getClientId());
        // If the client is unknown, respond as such.
        if (thirdParty == null) {
            // Create the OAuth response.
            OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(TokenResponse.INVALID_CLIENT)
                    .setErrorDescription("The client is unknown: " + oauthRequest.getClientId()).buildJSONMessage();

            // Set the status and return the error message.
            response.setStatus(oauthResponse.getResponseStatus());
            return oauthResponse.getBody();
        }

        // Get the given client secret.
        String thirdPartySecret = oauthRequest.getClientSecret();
        if (thirdPartySecret == null) {
            // Create the OAuth response.
            OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(TokenResponse.INVALID_CLIENT).setErrorDescription("The client secret is required.")
                    .buildJSONMessage();

            // Set the status and return the error message.
            response.setStatus(oauthResponse.getResponseStatus());
            return oauthResponse.getBody();
        }
        // Make sure the client gave the right secret.
        else if (!thirdPartySecret.equals(thirdParty.getSecret())) {
            // Create the OAuth response.
            OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(TokenResponse.INVALID_CLIENT).setErrorDescription("The client secret is incorrect.")
                    .buildJSONMessage();

            // Set the status and return the error message.
            response.setStatus(oauthResponse.getResponseStatus());
            return oauthResponse.getBody();
        }

        // Get the grant-type.
        GrantType grantType;
        String grantTypeString = oauthRequest.getGrantType();
        if (GrantType.AUTHORIZATION_CODE.toString().equals(grantTypeString)) {
            grantType = GrantType.AUTHORIZATION_CODE;
        } else if (GrantType.CLIENT_CREDENTIALS.toString().equals(grantTypeString)) {
            grantType = GrantType.CLIENT_CREDENTIALS;
        } else if (GrantType.PASSWORD.toString().equals(grantTypeString)) {
            grantType = GrantType.PASSWORD;
        } else if (GrantType.REFRESH_TOKEN.toString().equals(grantTypeString)) {
            grantType = GrantType.REFRESH_TOKEN;
        } else {
            // Create the OAuth response.
            OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(TokenResponse.INVALID_GRANT)
                    .setErrorDescription("The grant type is unknown: " + grantTypeString).buildJSONMessage();

            // Set the status and return the error message.
            response.setStatus(oauthResponse.getResponseStatus());
            return oauthResponse.getBody();
        }

        // Handle the different types of token requests.
        AuthorizationToken token;
        if (GrantType.AUTHORIZATION_CODE.equals(grantType)) {
            // Attempt to get the code.
            String codeString = oauthRequest.getCode();
            if (codeString == null) {
                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(TokenResponse.INVALID_REQUEST)
                        .setErrorDescription("An authorization code must be given to be "
                                + "exchanged for an authorization token.")
                        .buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }

            // Attempt to lookup the actual AuthorizationCode object.
            AuthorizationCode code = AuthorizationCodeBin.getInstance().getCode(codeString);
            // If the code doesn't exist, reject the request.
            if (code == null) {
                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(TokenResponse.INVALID_REQUEST)
                        .setErrorDescription("The given authorization code is unknown: " + codeString)
                        .buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }

            // Verify that the client asking for a token is the same as the one
            // that requested the code.
            if (!code.getThirdParty().getId().equals(thirdParty.getId())) {
                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(TokenResponse.INVALID_REQUEST)
                        .setErrorDescription(
                                "This client is not allowed to reference this " + "code: " + codeString)
                        .buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }

            // If the code has expired, reject the request.
            if (System.currentTimeMillis() > code.getExpirationTime()) {
                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(TokenResponse.INVALID_REQUEST)
                        .setErrorDescription("The given authorization code has expired: " + codeString)
                        .buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }

            // Use the code to lookup the response information and error out if
            // a user has not yet verified it.
            AuthorizationCodeResponse codeResponse = AuthorizationCodeResponseBin.getInstance()
                    .getResponse(code.getCode());
            if (codeResponse == null) {
                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(TokenResponse.INVALID_REQUEST)
                        .setErrorDescription("A user has not yet verified the code: " + codeString)
                        .buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }

            // Determine if the user granted access and, if not, error out.
            if (!codeResponse.getGranted()) {
                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(TokenResponse.INVALID_REQUEST)
                        .setErrorDescription("The user denied the authorization: " + codeString).buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }

            // Create a new token.
            token = new AuthorizationToken(codeResponse);
        }
        // Handle a third-party refreshing an existing token.
        else if (GrantType.REFRESH_TOKEN.equals(grantType)) {
            // Get the refresh token from the request.
            String refreshToken = oauthRequest.getRefreshToken();
            if (refreshToken == null) {
                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(TokenResponse.INVALID_REQUEST)
                        .setErrorDescription("An refresh token must be given to be exchanged "
                                + "for a new authorization token.")
                        .buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }
            // Use the refresh token to lookup the actual refresh token.
            AuthorizationToken currentToken = AuthorizationTokenBin.getInstance()
                    .getTokenFromRefreshToken(refreshToken);
            if (currentToken == null) {
                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(TokenResponse.INVALID_REQUEST)
                        .setErrorDescription("The refresh token is unknown.").buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }

            // Verify that the client asking for a token is the same as the one
            // that was issued the refresh token.
            // This is probably a very serious offense and should probably
            // raise some serious red flags!
            if (!currentToken.getThirdParty().getId().equals(thirdParty.getId())) {

                // Create the OAuth response.
                OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(TokenResponse.INVALID_REQUEST)
                        .setErrorDescription("This token does not belong to this client.").buildJSONMessage();

                // Set the status and return the error message.
                response.setStatus(oauthResponse.getResponseStatus());
                return oauthResponse.getBody();
            }

            // Create a new authorization token from the current one.
            token = new AuthorizationToken(currentToken);
        }
        // If the grant-type is unknown, then we do not yet understand how
        // the request is built and, therefore, can do nothing more than
        // reject it via an OmhException.
        else {
            // Create the OAuth response.
            OAuthResponse oauthResponse = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(TokenResponse.UNSUPPORTED_GRANT_TYPE)
                    .setErrorDescription("The grant type must be one of '" + GrantType.AUTHORIZATION_CODE.toString()
                            + "' or '" + GrantType.REFRESH_TOKEN.toString() + "': " + grantType.toString())
                    .buildJSONMessage();

            // Set the status and return the error message.
            response.setStatus(oauthResponse.getResponseStatus());
            return oauthResponse.getBody();
        }

        // Store the new token.
        AuthorizationTokenBin.getInstance().storeToken(token);

        // Build the response.
        OAuthResponse oauthResponse = OAuthASResponse.tokenResponse(HttpServletResponse.SC_OK)
                .setAccessToken(token.getAccessToken())
                .setExpiresIn(Long.valueOf(token.getExpirationIn() / 1000).toString())
                .setRefreshToken(token.getRefreshToken()).setTokenType(TokenType.BEARER.toString())
                .buildJSONMessage();

        // Set the status.
        response.setStatus(oauthResponse.getResponseStatus());

        // Set the content-type.
        response.setContentType("application/json");

        // Add the headers.
        Map<String, String> headers = oauthResponse.getHeaders();
        for (String headerKey : headers.keySet()) {
            response.addHeader(headerKey, headers.get(headerKey));
        }

        // Return the body.
        return oauthResponse.getBody();
    }

    /**
     * <p>
     * Creates a new user in the database and sends an activation email.
     * </p>
     * 
     * <p>
     * For systems that already have their own user activation/management
     * system in place, either remove this function or simply remove the
     * RequestMapping annotation.
     * </p>
     * 
     * @param username
     *        The new user's username.
     *        
     * @param password
     *        The new user's password
     *        
     * @param email The new user's email address.
     * 
     * @param request
     *        The HTTP request object.
     * 
     * @param response
     *        The HTTP response object.
     */
    @RequestMapping(value = UserRegistrationRequest.PATH, method = RequestMethod.POST)
    public @ResponseBody void registerUser(
            @RequestParam(value = User.JSON_KEY_USERNAME, required = true) final String username,
            @RequestParam(value = User.JSON_KEY_PASSWORD, required = true) final String password,
            @RequestParam(value = User.JSON_KEY_EMAIL, required = true) final String email,
            final HttpServletRequest request, final HttpServletResponse response) {

        handleRequest(request, response,
                new UserRegistrationRequest(username, password, email, buildRootUrl(request)));
    }

    /**
     * <p>
     * Creates a new user in the database and sends an activation email.
     * </p>
     * 
     * <p>
     * For systems that already have their own user activation/management
     * system in place, either remove this function or simply remove the
     * RequestMapping annotation.
     * </p>
     * 
     * @param username
     *        The new user's username.
     *        
     * @param password
     *        The new user's password
     *        
     * @param email The new user's email address.
     * 
     * @param request
     *        The HTTP request object.
     * 
     * @param response
     *        The HTTP response object.
     */
    @RequestMapping(value = UserActivationRequest.PATH, method = RequestMethod.POST)
    public @ResponseBody void activateUser(
            @RequestParam(value = User.JSON_KEY_REGISTRATION_KEY, required = true) final String registrationId,
            final HttpServletRequest request, final HttpServletResponse response) {

        handleRequest(request, response, new UserActivationRequest(registrationId));
    }

    @RequestMapping(value = "testing", method = RequestMethod.GET)
    public @ResponseBody User testing() {
        return UserBin.getInstance().getUser("sink.thaw");
    }

    /**
     * If the root of the hierarchy is requested, return the registry, which is
     * a map of all of the schema IDs to their high-level information, e.g.
     * name, description, latest version, etc.
     * 
     * @param numToSkip
     *        The number of data points to skip to facilitate paging.
     * 
     * @param numToReturn
     *        The number of data points to return to facilitate paging.
     * 
     * @param request
     *        The HTTP request object.
     * 
     * @param response
     *        The HTTP response object.
     * 
     * @return An array of all of the known schemas, limited by paging.
     */
    @RequestMapping(value = { "", "/" }, method = RequestMethod.GET)
    public @ResponseBody MultiValueResult<String> getSchemaIds(
            @RequestParam(value = PARAM_PAGING_NUM_TO_SKIP, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_SKIP_STRING) final long numToSkip,
            @RequestParam(value = PARAM_PAGING_NUM_TO_RETURN, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_RETURN_STRING) final long numToReturn,
            final HttpServletRequest request, final HttpServletResponse response) {

        return handleRequest(request, response, new SchemaIdsRequest(numToSkip, numToReturn));
    }

    /**
     * Creates a request to get the information about the given schema ID, e.g.
     * the name, description, version list, etc.
     * 
     * @param schemaId
     *        The schema ID from the URL.
     * 
     * @param numToSkip
     *        The number of data points to skip to facilitate paging.
     * 
     * @param numToReturn
     *        The number of data points to return to facilitate paging.
     * 
     * @param response
     *        The HTTP response object.
     * 
     * @return An array of schemas, one for each version of the given schema
     *         ID.
     */
    @RequestMapping(value = "{" + PARAM_SCHEMA_ID + "}", method = RequestMethod.GET)
    public @ResponseBody MultiValueResult<Long> getSchemaVersions(
            @PathVariable(PARAM_SCHEMA_ID) final String schemaId,
            @RequestParam(value = PARAM_PAGING_NUM_TO_SKIP, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_SKIP_STRING) final long numToSkip,
            @RequestParam(value = PARAM_PAGING_NUM_TO_RETURN, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_RETURN_STRING) final long numToReturn,
            final HttpServletRequest request, final HttpServletResponse response) {

        return handleRequest(request, response, new SchemaVersionsRequest(schemaId, numToSkip, numToReturn));
    }

    /**
     * Creates a request to get the definition of a specific schema ID's
     * version.
     * 
     * @param schemaId
     *        The schema ID from the URL.
     * 
     * @param version
     *        The schema version from the URL.
     * 
     * @param response
     *        The HTTP response object.
     * 
     * @return The schema for the given schema ID-version pair.
     */
    @RequestMapping(value = "{" + PARAM_SCHEMA_ID + "}/" + "{" + PARAM_SCHEMA_VERSION + ":[\\d]"
            + "}", method = RequestMethod.GET)
    public @ResponseBody Object getDefinition(@PathVariable(PARAM_SCHEMA_ID) final String schemaId,
            @PathVariable(PARAM_SCHEMA_VERSION) final Long version, final HttpServletRequest request,
            final HttpServletResponse response) {

        return handleRequest(request, response, new SchemaRequest(schemaId, version));
    }

    /**
     * Retrieves the requested data.
     * 
     * @param schemaId
     *        The ID for the schema to which the data pertains. This is part of
     *        the request's path.
     * 
     * @param version
     *        The version of the schema to which the data pertains. This is
     *        part of the request's path.
     * 
     * @param owner
     *        The user that owns the desired data.
     * 
     * @param columnList
     *        The list of columns to return to the user.
     * 
     * @param numToSkip
     *        The number of data points to skip to facilitate paging.
     * 
     * @param numToReturn
     *        The number of data points to return to facilitate paging.
     *        
     * @param request
     *        The HTTP request object.
     * 
     * @param response
     *        The HTTP response object.
     * 
     * @return The data as a JSON array of JSON objects where each object
     *         represents a single data point.
     * 
     * @see Data
     */
    @RequestMapping(value = "{" + PARAM_SCHEMA_ID + "}/{" + PARAM_SCHEMA_VERSION
            + "}/data", method = RequestMethod.GET)
    public @ResponseBody MultiValueResult<Data> getData(@PathVariable(PARAM_SCHEMA_ID) final String schemaId,
            @PathVariable(PARAM_SCHEMA_VERSION) final Long version,
            @RequestParam(value = PARAM_OWNER, required = false) final String owner,
            @RequestParam(value = PARAM_COLUMN_LIST, required = false) final List<String> columnList,
            @RequestParam(value = PARAM_PAGING_NUM_TO_SKIP, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_SKIP_STRING) final long numToSkip,
            @RequestParam(value = PARAM_PAGING_NUM_TO_RETURN, required = false, defaultValue = ListRequest.DEFAULT_NUMBER_TO_RETURN_STRING) final long numToReturn,
            final HttpServletRequest request, final HttpServletResponse response) {

        // Handle the request.
        return handleRequest(request, response,
                new DataReadRequest(
                        (AuthenticationToken) request.getAttribute(AuthFilter.ATTRIBUTE_AUTHENTICATION_TOKEN),
                        (AuthorizationToken) request.getAttribute(AuthFilter.ATTRIBUTE_AUTHORIZATION_TOKEN),
                        schemaId, version, owner, columnList, numToSkip, numToReturn));
    }

    /**
     * Writes the requested data.
     * 
     * @param schemaId
     *        The ID for the schema to which the data pertains.
     * 
     * @param version
     *        The version of the schema to which the data pertains.
     *        
     * @param data
     *        The data to be uploaded, which should be a JSON array of JSON
     *        objects where each object is a single data point.
     *        
     * @param request
     *        The HTTP request object.
     * 
     * @param response
     *        The HTTP response object.
     */
    @RequestMapping(value = "{" + PARAM_SCHEMA_ID + "}/{" + PARAM_SCHEMA_VERSION
            + "}/data", method = RequestMethod.POST)
    public void putData(@PathVariable(PARAM_SCHEMA_ID) final String schemaId,
            @PathVariable(PARAM_SCHEMA_VERSION) final Long version,
            @RequestParam(value = PARAM_DATA, required = true) final String data, final HttpServletRequest request,
            final HttpServletResponse response) {

        // Make sure the authentication token was a parameter. This prevents
        // malicious code from "hijacking" the token by performing a POST and
        // having the browser inject it as only a cookie.
        Object authenticationTokenIsParam = request
                .getAttribute(AuthFilter.ATTRIBUTE_AUTHENTICATION_TOKEN_IS_PARAM);
        if ((authenticationTokenIsParam == null) || (!((Boolean) authenticationTokenIsParam))) {

            throw new OmhException("To upload data, the authentication token is required " + "as a parameter.");
        }

        // Get the authentication token.
        AuthenticationToken authToken = (AuthenticationToken) request
                .getAttribute(AuthFilter.ATTRIBUTE_AUTHENTICATION_TOKEN);

        // Handle the request.
        handleRequest(request, response, new DataWriteRequest(authToken, schemaId, version, data));
    }

    /**
     * Handles a request then sets the meta-data as HTTP headers and returns
     * the data to be returned to the user.
     * 
     * @param httpRequest
     *        The HTTP request.
     * 
     * @param httpResponse
     *        The HTTP response.
     * 
     * @param request
     *        The already-built, domain-specific request to be serviced.
     * 
     * @return The object to be returned to the user.
     */
    private <T> T handleRequest(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse,
            final Request<? extends T> request) {

        // Service the request.
        request.service();

        // Retrieve the meta-data and add it as HTTP headers.
        Map<String, Object> metaData = request.getMetaData();
        if (metaData != null) {
            for (String metaDataKey : metaData.keySet()) {
                httpResponse.setHeader(metaDataKey, metaData.get(metaDataKey).toString());
            }
        }

        // If this is a list request, add the next and previous parameters.
        if (request instanceof ListRequest) {
            // Create the previous and next headers, if appropriate.
            addNextPreviousHeaders(httpRequest, httpResponse, (ListRequest<?>) request);
        }

        // Return the data.
        return request.getData();
    }

    /**
     * Builds the base URL for the request that came in. This is everything up
     * to our web applications base, e.g. "http://localhost:8080/omh".
     * 
     * @param httpRequest
     *        The original HTTP request.
     * 
     * @return The base URL for the request.
     */
    private String buildRootUrl(final HttpServletRequest httpRequest) {
        // It must be a HTTP request.
        StringBuilder builder = new StringBuilder("http");

        // If security was used add the "s" to make it "https".
        if (httpRequest.isSecure()) {
            builder.append('s');
        }

        // Add the protocol separator.
        builder.append("://");

        // Add the name of the server where the request was sent.
        builder.append(httpRequest.getServerName());

        // Add the port separator and the port.
        builder.append(':').append(httpRequest.getServerPort());

        // Add the context path, e.g. "/omh".
        builder.append(httpRequest.getContextPath());

        // Return the root URL.
        return builder.toString();
    }

    /**
     * <p>
     * Builds the URL used to make this request based on the request.
     * </p>
     * 
     * <p>
     * The URL is built from all of the information Java provides about the
     * system including the hostname. However, in a distributed environment,
     * this may not be adequate or correct. 
     * </p>
     * 
     * @param httpRequest
     *        The original HTTP request.
     * 
     * @return The base URL used to make the request that is calling this
     *         function.
     */
    private String buildRequestUrl(final HttpServletRequest httpRequest) {
        // Start with the root URL.
        StringBuilder builder = new StringBuilder(buildRootUrl(httpRequest));

        // Add the specific path in the request.
        builder.append(httpRequest.getPathInfo());

        // Return the base URL, which should only need to have the parameters
        // added to it.
        return builder.toString();
    }

    /**
     * Creates and adds the Previous and Next headers.
     * 
     * @param httpRequest
     *        The HTTP request.
     * 
     * @param httpResponse
     *        The HTTP response.
     * 
     * @param listRequest
     *        The ListRequest used to get the paging headers.
     */
    private void addNextPreviousHeaders(final HttpServletRequest httpRequest,
            final HttpServletResponse httpResponse, final ListRequest<?> listRequest) {

        // Get the new set of parameters.
        Map<String, String> parameters = listRequest.getPreviousNextParameters();

        // If we skipped any data, create a Previous header.
        if (listRequest.getNumToSkip() > 0) {
            // Build the base URL.
            StringBuilder previousBuilder = new StringBuilder(buildRequestUrl(httpRequest));

            // Add the query separator.
            previousBuilder.append('?');

            // Use a try-catch in case our encoding, which is the same for
            // each parameter, is unknown.
            try {
                // Add each of the custom parameters.
                boolean firstPass = true;
                for (String parameterKey : parameters.keySet()) {
                    // Add the parameter separator.
                    if (firstPass) {
                        firstPass = false;
                    } else {
                        previousBuilder.append('&');
                    }

                    // Add the parameter.
                    previousBuilder.append(URLEncoder.encode(parameterKey, URL_ENCODING_UTF_8)).append('=')
                            .append(URLEncoder.encode(parameters.get(parameterKey), URL_ENCODING_UTF_8));
                }

                // Add the paging parameters.
                if (parameters.size() > 0) {
                    previousBuilder.append('&');
                }

                // Calculate the previous number to skip.
                long previousNumToSkip = listRequest.getNumToSkip() - listRequest.getNumToReturn();
                // If the previous number to skip is greater than zero, add
                // the number to skip.
                if (previousNumToSkip > 0) {
                    previousBuilder.append(URLEncoder.encode(PARAM_PAGING_NUM_TO_SKIP, URL_ENCODING_UTF_8))
                            .append('=')
                            .append(URLEncoder.encode(Long.toString(previousNumToSkip), URL_ENCODING_UTF_8));
                    previousBuilder.append('&');
                }

                // Always add the number to return.
                previousBuilder.append(URLEncoder.encode(PARAM_PAGING_NUM_TO_RETURN, URL_ENCODING_UTF_8))
                        .append('=')
                        .append(URLEncoder.encode(
                                Long.toString(Math.min(listRequest.getNumToSkip(), listRequest.getNumToReturn())),
                                URL_ENCODING_UTF_8));
            } catch (UnsupportedEncodingException e) {
                LOGGER.log(Level.SEVERE,
                        "The encoding is unknown so the " + HEADER_PREVIOUS + " header could not be built.");
            }

            // Add the previous header.
            httpResponse.setHeader(HEADER_PREVIOUS, previousBuilder.toString());
        }

        // If the total data-set size is greater than the number of points
        // skipped plus the number of points requested, then there must be more
        // data, and a Next header should be added.
        if (listRequest.getData().count() > (listRequest.getNumToSkip() + listRequest.getNumToReturn())) {

            // Build the base URL.
            StringBuilder nextBuilder = new StringBuilder(buildRequestUrl(httpRequest));

            // Add the query separator.
            nextBuilder.append('?');

            // Use a try-catch in case our encoding, which is the same for
            // each parameter, is unknown.
            try {
                // Add each of the custom parameters.
                boolean firstPass = true;
                for (String parameterKey : parameters.keySet()) {
                    // Add the parameter separator.
                    if (firstPass) {
                        firstPass = false;
                    } else {
                        nextBuilder.append('&');
                    }

                    // Add the parameter.
                    nextBuilder.append(URLEncoder.encode(parameterKey, URL_ENCODING_UTF_8)).append('=')
                            .append(URLEncoder.encode(parameters.get(parameterKey), URL_ENCODING_UTF_8));
                }

                // Add the paging parameters.
                if (parameters.size() > 0) {
                    nextBuilder.append('&');
                }

                // Calculate the previous number to skip.
                long nextNumToSkip = listRequest.getNumToSkip() + listRequest.getNumToReturn();
                // Always add the number to skip.
                nextBuilder.append(URLEncoder.encode(PARAM_PAGING_NUM_TO_SKIP, URL_ENCODING_UTF_8)).append('=')
                        .append(URLEncoder.encode(Long.toString(nextNumToSkip), URL_ENCODING_UTF_8));

                // Add the parameter separator.
                nextBuilder.append('&');

                // Always add the number to return.
                nextBuilder.append(URLEncoder.encode(PARAM_PAGING_NUM_TO_RETURN, URL_ENCODING_UTF_8)).append('=')
                        .append(URLEncoder.encode(Long.toString(listRequest.getNumToReturn()), URL_ENCODING_UTF_8));
            } catch (UnsupportedEncodingException e) {
                LOGGER.log(Level.SEVERE,
                        "The encoding is unknown so the " + HEADER_NEXT + " header could not be built.");
            }

            // Add the previous header.
            httpResponse.setHeader(HEADER_NEXT, nextBuilder.toString());
        }
    }
}