org.sventon.web.ctrl.template.AbstractTemplateController.java Source code

Java tutorial

Introduction

Here is the source code for org.sventon.web.ctrl.template.AbstractTemplateController.java

Source

/*
 * ====================================================================
 * Copyright (c) 2005-2012 sventon project. All rights reserved.
 *
 * This software is licensed as described in the file LICENSE, which
 * you should have received as part of this distribution. The terms
 * are also available at http://www.sventon.org.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.sventon.web.ctrl.template;

import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import org.sventon.AuthenticationException;
import org.sventon.SVNConnection;
import org.sventon.SventonException;
import org.sventon.appl.RepositoryConfiguration;
import org.sventon.cache.CacheGateway;
import org.sventon.diff.DiffException;
import org.sventon.model.*;
import org.sventon.web.UserRepositoryContext;
import org.sventon.web.command.BaseCommand;
import org.sventon.web.ctrl.AbstractBaseController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.springframework.web.bind.ServletRequestUtils.*;

/**
 * Abstract base class for use by controllers wishing to make use of basic
 * plumbing functionality such as authorization and basic application configuration.
 * <p/>
 * This abstract controller is based on the GoF Template pattern, the method to
 * implement for extending controllers is
 * <code>{@link #svnHandle(SVNConnection, org.sventon.web.command.BaseCommand, long, UserRepositoryContext,
 * HttpServletRequest, HttpServletResponse, BindException)}</code>.
 * <p/>
 * Workflow for this controller:
 * <ol>
 * <li>The controller inspects the application configuration object to see if it's
 * user id and password have been provided during setup. If credentials are
 * configured they will be used for authorized repository access, if they do not
 * exist the controller will try to set up the repository with anonymous access.
 * If this fails the user will be forwarded to an error page.
 * <li>The controller configures the <code>SVNRepository</code> object and
 * calls the extending class'
 * {@link #svnHandle(SVNConnection, org.sventon.web.command.BaseCommand, long, UserRepositoryContext,
 * HttpServletRequest, HttpServletResponse, BindException)}
 * method with the given {@link org.sventon.web.command.BaseCommand}
 * containing request parameters.
 * <li>After the call returns, the controller adds additional information to
 * the the model (see below) and forwards the request to the view returned
 * together with the model by the
 * {@link #svnHandle(SVNConnection, org.sventon.web.command.BaseCommand, long, org.sventon.web.UserRepositoryContext,
 * HttpServletRequest, HttpServletResponse, BindException)}
 * method.
 * </ol>
 * <b>Input arguments</b><br>
 * Input to this argument is wrapped in a
 * <code>{@link org.sventon.web.command.BaseCommand}</code> object by the
 * Spring framework. If the extending controller is configured in the Spring
 * config file with a validator for the <code>BaseCommand</code> it will be
 * checked for binding errors. If binding errors were detected an exception
 * model will be created an control forwarded to an error view respectively.
 * <b>Exception handling</b>
 * <dl>
 * <dt>Authentication exception
 * <dd>If a SVN authentication exception occurs during the call the request
 * will be forwarded to the 'authenticationfailure.jsp' page.
 * <dt>Other SVN exceptions
 * <dd>Other SVN exceptions are currently forwarded to a generic error handling
 * page.
 * </dl>
 *
 * @author patrik@sventon.org
 * @author jesper@sventon.org
 */
public abstract class AbstractTemplateController extends AbstractBaseController {

    /**
     * Gateway class for accessing the caches.
     */
    private CacheGateway cacheGateway;

    /**
     * Maximum number of revisions.
     */
    private int maxRevisionsCount;

    /**
     * Maximum number of entries per page.
     */
    private int maxEntriesCount;

    /**
     * Destination view name.
     */
    private String viewName;

    /**
     * Cached available charsets.
     */
    private AvailableCharsets availableCharsets;

    /**
     * Mode for searching entries.
     */
    public static final String ENTRIES_SEARCH_MODE = "entries";

    /**
     * Mode for searching log messages.
     */
    public static final String LOGMESSAGES_SEARCH_MODE = "logMessages";

    /**
     * Request parameter controlling charset.
     */
    private static final String CHARSET_REQUEST_PARAMETER = "charset";

    /**
     * Request parameter controlling search mode.
     */
    private static final String SEARCH_MODE_REQUEST_PARAMETER = "searchMode";

    /**
     * Request parameter controlling revision count.
     */
    private static final String REVISION_COUNT_REQUEST_PARAMETER = "revcount";

    @Override
    public final ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response,
            final Object cmd, final BindException errors) {

        final BaseCommand command = (BaseCommand) cmd;
        logger.debug("'command' set to: " + command);

        // If application config is not ok - redirect to config
        if (!application.isConfigured()) {
            logger.debug("sventon not configured, redirecting to '/repos/listconfigs'");
            return new ModelAndView(new RedirectView("/repos/listconfigs", true));
        }

        if (command.getName() == null || !application.getRepositoryNames().contains(command.getName())) {
            logger.debug("RepositoryName [" + command.getName() + "] does not exist, redirecting to '/repos/list'");
            return new ModelAndView(new RedirectView("/repos/list", true));
        }

        if (errors.hasErrors()) {
            return prepareExceptionModelAndView(errors, command, getApplicationModel(command));
        }

        SVNConnection connection = null;

        try {
            final RepositoryConfiguration configuration = application.getConfiguration(command.getName());
            final UserRepositoryContext repositoryContext = UserRepositoryContext.getContext(request,
                    command.getName());
            final boolean showLatestRevInfo = getBooleanParameter(request, "showlatestrevinfo", false);

            connection = createConnection(configuration, repositoryContext);
            final Long headRevision = getRepositoryService().getLatestRevision(connection);
            command.setRevision(
                    getRepositoryService().translateRevision(connection, command.getRevision(), headRevision));

            parseAndUpdateSortParameters(command, repositoryContext);
            parseAndUpdateLatestRevisionsDisplayCount(request, repositoryContext);
            parseAndUpdateCharsetParameter(request, repositoryContext);
            parseAndUpdateSearchModeParameter(request, repositoryContext);

            final ModelAndView modelAndView = svnHandle(connection, command, headRevision, repositoryContext,
                    request, response, errors);

            // It's ok for svnHandle to return null in cases like GetFileController.
            if (needModelPopulation(modelAndView)) {
                final Map<String, Object> model = new HashMap<String, Object>(getApplicationModel(command));
                model.put("userRepositoryContext", repositoryContext);
                model.put("useCache", configuration.isCacheUsed());
                model.put("repositoryURL", configuration.getRepositoryDisplayUrl());
                model.put("isEntryTrayEnabled", configuration.isEntryTrayEnabled());
                model.put("isZipDownloadsAllowed", configuration.isZippedDownloadsAllowed());
                model.put("headRevision", headRevision);
                model.put("isHead", command.getRevisionNumber() == headRevision);
                if (showLatestRevInfo) {
                    model.put("revisions", getLatestRevisions(command, connection, repositoryContext));
                }
                modelAndView.addAllObjects(model);
            }
            return modelAndView;
        } catch (AuthenticationException ae) {
            logger.debug(ae.getMessage());
            return prepareAuthenticationRequiredView(request, getApplicationModel(command));
        } catch (DiffException ex) {
            logger.warn(ex.getMessage());
            errors.reject(null, ex.getMessage());
            return prepareExceptionModelAndView(errors, command, getApplicationModel(command));
        } catch (Exception ex) {
            logger.error("Exception", ex);
            final Throwable cause = ex.getCause();
            if (cause instanceof NoRouteToHostException || cause instanceof ConnectException) {
                errors.reject("error.message.no-route-to-host");
            } else {
                errors.reject(null, ex.getMessage());
            }
            return prepareExceptionModelAndView(errors, command, getApplicationModel(command));
        } finally {
            if (connection != null) {
                connection.closeSession();
            }
        }
    }

    protected Map<String, Object> getApplicationModel(final BaseCommand command) {
        final Map<String, Object> applicationModel = new HashMap<String, Object>();
        applicationModel.put("baseURL", application.getBaseURL());
        applicationModel.put("isUpdating", application.isUpdating(command.getName()));
        applicationModel.put("repositoryNames", application.getRepositoryNames());
        applicationModel.put("isEditableConfig", application.isEditableConfig());
        applicationModel.put("charsets", availableCharsets.getCharsets());
        applicationModel.put("maxRevisionsCount", getMaxRevisionsCount());
        applicationModel.put("command", command);
        return applicationModel;
    }

    // If the view is a RedirectView it's model has already been populated

    private boolean needModelPopulation(ModelAndView modelAndView) {
        return modelAndView != null && !(modelAndView.getView() instanceof RedirectView);
    }

    private List<LogEntry> getLatestRevisions(BaseCommand command, SVNConnection connection,
            UserRepositoryContext repositoryContext) throws SventonException {
        logger.debug("Fetching [" + repositoryContext.getLatestRevisionsDisplayCount()
                + "] latest revisions for display");
        return getRepositoryService().getLatestRevisions(connection, command.getName(),
                repositoryContext.getLatestRevisionsDisplayCount());
    }

    /**
     * Creates a repository connection.
     *
     * @param configuration         Configuration
     * @param userRepositoryContext Context
     * @return Connection
     * @throws SventonException if a subversion error occur.
     */
    protected SVNConnection createConnection(final RepositoryConfiguration configuration,
            final UserRepositoryContext userRepositoryContext) throws SventonException {
        final SVNConnection connection;
        final RepositoryName repositoryName = configuration.getName();
        final SVNURL svnurl = configuration.getSVNURL();

        if (configuration.isAccessControlEnabled()) {
            connection = connectionFactory.createConnection(repositoryName, svnurl,
                    userRepositoryContext.getCredentials());
        } else {
            connection = connectionFactory.createConnection(repositoryName, svnurl,
                    configuration.getUserCredentials());
        }
        return connection;
    }

    /**
     * Parses the parameter controlling what charset to use.
     *
     * @param request     The request.
     * @param userContext The UserContext instance to update.
     */
    private void parseAndUpdateCharsetParameter(final HttpServletRequest request,
            final UserRepositoryContext userContext) {
        final String charset = getStringParameter(request, CHARSET_REQUEST_PARAMETER, null);
        if (charset != null) {
            userContext.setCharset(charset);
        } else if (userContext.getCharset() == null) {
            userContext.setCharset(availableCharsets.getDefaultCharset());
        }
    }

    /**
     * Parses the parameter controlling what search mode to use.
     *
     * @param request     The request.
     * @param userContext The UserContext instance to update.
     */
    private void parseAndUpdateSearchModeParameter(final HttpServletRequest request,
            final UserRepositoryContext userContext) {
        final String searchMode = getStringParameter(request, SEARCH_MODE_REQUEST_PARAMETER, null);
        if (searchMode != null) {
            userContext.setSearchMode(searchMode);
        } else if (userContext.getSearchMode() == null) {
            userContext.setSearchMode(ENTRIES_SEARCH_MODE);
        }
    }

    /**
     * Parses the parameter controlling how many revisions should be displayed in the
     * <i>latest commit info</i> DIV.
     *
     * @param request     The request.
     * @param userContext The UserContext instance to update.
     */
    private void parseAndUpdateLatestRevisionsDisplayCount(final HttpServletRequest request,
            final UserRepositoryContext userContext) {
        final int latestRevisionsDisplayCount = getIntParameter(request, REVISION_COUNT_REQUEST_PARAMETER, 0);
        if (latestRevisionsDisplayCount <= getMaxRevisionsCount() && latestRevisionsDisplayCount >= 0) {
            if (latestRevisionsDisplayCount > 0) {
                userContext.setLatestRevisionsDisplayCount(latestRevisionsDisplayCount);
            }
        } else {
            throw new IllegalArgumentException("Illegal revision count: " + latestRevisionsDisplayCount);
        }
    }

    /**
     * Parses sort mode and type parameters from the request instance and
     * updates the <code>UserContext</code> instance.
     *
     * @param command     The Command.
     * @param userContext The UserContext instance to update.
     */
    protected final void parseAndUpdateSortParameters(final BaseCommand command,
            final UserRepositoryContext userContext) {

        if (command.getSortType() != null) {
            userContext.setSortType(command.getSortType());
        } else if (userContext.getSortType() == null) {
            userContext.setSortType(DirEntryComparator.SortType.FULL_NAME);
        }

        if (command.getSortMode() != null) {
            userContext.setSortMode(command.getSortMode());
        } else if (userContext.getSortMode() == null) {
            userContext.setSortMode(DirEntrySorter.SortMode.ASC);
        }
    }

    /**
     * Prepares the authentication model. This sets up a model and redirect view with
     * all stuff needed to redirect control to the login page.
     *
     * @param request Request.
     * @param model   Pre-populated model.
     * @return Redirect view for logging in, with original request info stored in
     *         session to enable the authentication control to proceed with
     *         original request once the user is authenticated.
     */
    private ModelAndView prepareAuthenticationRequiredView(final HttpServletRequest request,
            Map<String, Object> model) {
        final Map<String, Object> authenticationModel = new HashMap<String, Object>(model);
        authenticationModel.put("parameters", request.getParameterMap());
        authenticationModel.put("action", request.getRequestURL());
        logger.debug("Forwarding to 'authenticationRequired' view");
        return new ModelAndView("error/authenticationRequired", authenticationModel);
    }

    /**
     * Prepares the exception model and view with basic data
     * needed to for displaying a useful error message.
     *
     * @param exception        Bind exception from Spring MVC validation.
     * @param command          Command object.
     * @param applicationModel Pre-populated model.
     * @return The packaged model and view.
     */
    @SuppressWarnings("unchecked")
    final ModelAndView prepareExceptionModelAndView(final BindException exception, final BaseCommand command,
            final Map<String, Object> applicationModel) {
        final RepositoryConfiguration repositoryConfiguration = application.getConfiguration(command.getName());
        final Map<String, Object> model = exception.getModel();
        model.putAll(applicationModel);
        model.put("repositoryURL",
                repositoryConfiguration != null ? repositoryConfiguration.getRepositoryDisplayUrl() : "");
        logger.debug("Exception model: " + model);
        return new ModelAndView("goto", model);
    }

    /**
     * Abstract method to be implemented by the controller sub classing this
     * controller. This is where the actual work takes place. See class
     * documentation for info on workflow and on how all this works together.
     *
     * @param connection            Reference to the repository, prepared with authentication
     *                              if applicable.
     * @param command               Command (basically request parameters submitted in user
     *                              request)
     * @param headRevision          The head revision.
     * @param userRepositoryContext The user's context instance for this repository.
     * @param request               Servlet request.
     * @param response              Servlet response.
     * @param exception             BindException, could be used by the subclass to add error
     *                              messages to the exception.
     * @return Model and view to render.
     * @throws Exception Thrown if exception occurs during SVN operations.
     */
    protected abstract ModelAndView svnHandle(final SVNConnection connection, final BaseCommand command,
            final long headRevision, final UserRepositoryContext userRepositoryContext,
            final HttpServletRequest request, final HttpServletResponse response, final BindException exception)
            throws Exception;

    /**
     * Sets the cache gateway instance.
     *
     * @param cacheGateway The cache gateway instance.
     */
    public final void setCacheGateway(final CacheGateway cacheGateway) {
        this.cacheGateway = cacheGateway;
    }

    /**
     * Gets the cache instance.
     *
     * @return The instance.
     */
    final CacheGateway getCache() {
        return cacheGateway;
    }

    /**
     * Sets the maximum number of revisions.
     *
     * @param maxRevisionsCount Max count.
     */
    public final void setMaxRevisionsCount(final int maxRevisionsCount) {
        this.maxRevisionsCount = maxRevisionsCount;
    }

    /**
     * Gets the maximum number of revisions a user can choose to display
     * in the <i>latest commit info</i> DIV.
     *
     * @return Count
     */
    final int getMaxRevisionsCount() {
        return maxRevisionsCount;
    }

    /**
     * Sets the maximum number of entries per page.
     *
     * @param maxEntriesCount Max count.
     */
    public final void setMaxEntriesCount(final int maxEntriesCount) {
        this.maxEntriesCount = maxEntriesCount;
    }

    /**
     * Gets the maximum number of entries per page.
     *
     * @return Count
     */
    protected final int getMaxEntriesCount() {
        return maxEntriesCount;
    }

    /**
     * Sets the available charsets.
     *
     * @param availableCharsets Charsets
     */
    public final void setAvailableCharsets(final AvailableCharsets availableCharsets) {
        this.availableCharsets = availableCharsets;
    }

    /**
     * Sets the destination view name.
     *
     * @param viewName View name.
     */
    public void setViewName(final String viewName) {
        this.viewName = viewName;
    }

    /**
     * Gets the destination view name.
     *
     * @return View name.
     */
    public String getViewName() {
        return viewName;
    }

}