com.matthewcasperson.validation.filter.ParameterValidationFilter.java Source code

Java tutorial

Introduction

Here is the source code for com.matthewcasperson.validation.filter.ParameterValidationFilter.java

Source

/*
 * The MIT License (MIT)
 * 
 * Copyright (c) 2014 Matthew Casperson
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.matthewcasperson.validation.filter;

import java.io.IOException;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.exception.ExceptionUtils;

import com.matthewcasperson.validation.exception.ValidationFailedException;
import com.matthewcasperson.validation.rule.ParameterValidationRule;
import com.matthewcasperson.validation.ruledefinitionimpl.ParameterValidationChain;
import com.matthewcasperson.validation.ruledefinitionimpl.ParameterValidationDefinitionImpl;
import com.matthewcasperson.validation.ruledefinitionimpl.ParameterValidationDefinitionsImpl;
import com.matthewcasperson.validation.utils.SerialisationUtils;
import com.matthewcasperson.validation.utilsimpl.JaxBSerialisationUtilsImpl;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * This filter intercepts the parameters sent by the client and cleans them up based on some
 * rules defined in a config file. This means any web application that sits behind this
 * filter can assume that the param object contains sanatised values.
 * @author mcasperson
 *
 */
public class ParameterValidationFilter implements Filter {
    private static final Logger LOGGER = Logger.getLogger(ParameterValidationFilter.class.getName());
    private static final SerialisationUtils SERIALISATION_UTILS = new JaxBSerialisationUtilsImpl();

    /**
     * This is the init-param name that we expect to hold a reference to the
     * config xml file.
     */
    private static final String CONFIG_PARAMETER_NAME = "configFile";

    /**
     * The list of validation rules that are to be applied 
     */
    private ParameterValidationDefinitionsImpl parameterValidationDefinitions;

    @Override
    public void destroy() {
        /*
         * Nothing to do here
         */
    }

    /**
     * This filter implements multiple chains of validation rules. Each chain is executed against each parameter until 
     * alll validation rules have been executed, or until one of the validation rules stops the execution of the chain.
     */
    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
            throws IOException, ServletException {

        //LOGGER.log(Level.INFO, "Parameter Validation Filter processing request");

        ServletRequest requestWrapper = request;

        try {
            if (parameterValidationDefinitions != null
                    && parameterValidationDefinitions.getParameterValidationDefinitions() != null) {

                LOGGER.log(Level.FINE, "Parameter Validation Filter has loaded the config file");

                if (requestWrapper instanceof HttpServletRequest) {

                    //LOGGER.log(Level.INFO, "Parameter Validation Filter is filtering a HttpServletRequest");

                    final HttpServletRequest httpServletRequest = (HttpServletRequest) requestWrapper;

                    /*
                     * Loop over each param. Note that while the validation rules may well
                     * create wrappers that return different values for the params (i.e. requestWrapper is
                     * updated to reference a new wrapper), we use this original copy for the list of 
                     * param keys to loop over.
                     */
                    final Enumeration<String> iter = httpServletRequest.getParameterNames();

                    paramaterNameLoop: while (iter.hasMoreElements()) {
                        /*
                         * Get the param name and move the enumerator along
                         */
                        final String paramName = iter.nextElement();

                        boolean paramValidated = false;

                        LOGGER.log(Level.FINE, "Parameter Validation Filter processing " + paramName);

                        /*
                         * Loop over each validation rule in the chain
                         */
                        final List<ParameterValidationChain> validationChains = parameterValidationDefinitions
                                .getParameterValidationDefinitions();
                        for (final ParameterValidationChain validationChain : validationChains) {

                            checkState(validationChain != null, "A validation rule should never be null");

                            /*
                             * Test this validation rule against the param name
                             */

                            final boolean paramMatches = validationChain.getParamNamePattern().matcher(paramName)
                                    .find();
                            final boolean uriMatches = validationChain.getRequestURIPattern()
                                    .matcher(httpServletRequest.getRequestURI()).find();

                            final boolean paramMatchesAfterNegation = paramMatches
                                    ^ validationChain.isParamNamePatternNegated();
                            final boolean uriMatchesAfterNegation = uriMatches
                                    ^ validationChain.isRequestURIPatternNegated();

                            if (paramMatchesAfterNegation && uriMatchesAfterNegation) {

                                LOGGER.log(Level.FINE, "Parameter Validation Filter found matching chain");

                                /*
                                 * Make a note that this parameter has been validated by at least one rule
                                 */
                                paramValidated = true;

                                /*
                                 * Loop over each rule in the chain 
                                 */
                                for (final ParameterValidationDefinitionImpl validationRule : validationChain
                                        .getList()) {
                                    LOGGER.log(Level.FINE, "Processing " + paramName + " with "
                                            + validationRule.getValidationRuleName());

                                    /*
                                     * Get the object that will actually do the validation
                                     */
                                    final ParameterValidationRule rule = validationRule.getRule();

                                    /*
                                     * It is possible that a bad configuration will result in rule being null
                                     */
                                    checkState(rule != null,
                                            "A validation rule should never be null. Check the class name defined in the configuration xml file.");

                                    try {
                                        /*
                                           Process the parameter
                                         */
                                        final ServletRequest processRequest = rule.processParameter(requestWrapper,
                                                paramName);

                                        checkState(processRequest != null,
                                                "A validation rule should never return null when processing a paramemter");

                                        /*
                                         * The validation rule is expected to return a valid request regardless of the
                                         * processing that should or should not be done.
                                         */
                                        requestWrapper = processRequest;
                                    } catch (final ValidationFailedException ex) {
                                        /*
                                         * Log this as a warning as we are probably interested in knowing when our apps
                                         * are getting hit with invalid data.
                                         */
                                        LOGGER.log(Level.WARNING, ex.toString());

                                        if (parameterValidationDefinitions.getEnforcingMode()) {
                                            /*
                                               If we are enforcing, rethrow so the outer catch block can block the
                                               request
                                             */
                                            throw ex;
                                        } else {
                                            /*
                                               Otherwise move to the next parameter name. This allows us to be notified
                                               of every param that will fail instead of just bailing with the first
                                               one that fails.
                                             */
                                            continue paramaterNameLoop;
                                        }
                                    }

                                }
                            }
                        }

                        if (!paramValidated) {
                            /*
                                * This might be intentional, so log it as an INFO
                                */
                            LOGGER.log(Level.INFO, "PVF-INFO-0001: " + paramName + " has not been validated.");
                        }
                    }
                }
            }
        } catch (final ValidationFailedException ex) {
            /*
             * Stop processing and return a HTTP error code if we are enforcing the rules
             */
            if (parameterValidationDefinitions.getEnforcingMode()) {
                respondWithBadRequest(response);
                return;
            }
        } catch (final Exception ex) {
            /*
             * We probably reach this because of some invalid state due to rules returning null
             * or throwing unchecked exceptions during their own processing. This is logged as
             * severe as it is most likely a bug in the code.
             */
            LOGGER.log(Level.SEVERE, ExceptionUtils.getFullStackTrace(ex));

            /*
             * Don't allow apps to process raw parameters if this filter has failed and we are
             * enforcing the rules
             */
            if (parameterValidationDefinitions.getEnforcingMode()) {
                respondWithBadRequest(response);
                return;
            }
        }

        /*
         * Continue to the next filter
         */
        if (parameterValidationDefinitions.getEnforcingMode()) {
            /*
               In enforcing mode we pass the wrapper onto the next filter
             */
            chain.doFilter(requestWrapper, response);
        } else {
            /*
               If enforcing mode is not enabled, we pass through the original request
             */
            chain.doFilter(request, response);
        }
    }

    /**
     * Return with a status code of 400
     * @param response The servlet request
     */
    private void respondWithBadRequest(final ServletResponse response) {
        checkNotNull(response);

        /*
         * This is thrown when one of the validation rules determined that a parameter was
         * sent with invalid data and could not, or should not, be sanitised.
         */
        if (response instanceof HttpServletResponse) {
            try {
                final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                httpServletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid parameter data");
            } catch (final IOException ex) {
                /*
                 * This shouldn't happen, but log it if it does
                 */
                LOGGER.log(Level.SEVERE, ExceptionUtils.getFullStackTrace(ex));
            }
        }
    }

    /**
     * Attempts to parse the XML config file. The config file is a JaxB serialisation of a
     * ParameterValidationDefinitionsImpl object. 
     */
    @Override
    public void init(final FilterConfig config) throws ServletException {
        try {
            final String configFile = config.getInitParameter(CONFIG_PARAMETER_NAME);
            if (configFile != null) {
                LOGGER.log(Level.FINE, "Attempting to unmarshall " + configFile);
                final String configXml = IOUtils
                        .toString(config.getServletContext().getResourceAsStream(configFile));
                LOGGER.log(Level.FINE, "configXml is \n" + configXml);
                parameterValidationDefinitions = SERIALISATION_UTILS.readFromXML(configXml,
                        ParameterValidationDefinitionsImpl.class);
            }
        } catch (final Exception ex) {
            /*
             * This will happen if the supplied XML is invalid. Log the error
             */
            LOGGER.log(Level.SEVERE, ExceptionUtils.getFullStackTrace(ex));

            /*
             * Rethrow as we don't want to proceed with invalid configuration
             */
            throw new ServletException(ex);
        }

    }

}