com.expressui.core.MainApplication.java Source code

Java tutorial

Introduction

Here is the source code for com.expressui.core.MainApplication.java

Source

/*
 * Copyright (c) 2012 Brown Bag Consulting.
 * This file is part of the ExpressUI project.
 * Author: Juan Osuna
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License Version 3
 * as published by the Free Software Foundation with the addition of the
 * following permission added to Section 15 as permitted in Section 7(a):
 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
 * Brown Bag Consulting, Brown Bag Consulting DISCLAIMS THE WARRANTY OF
 * NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license. Buying such a license is mandatory as soon as you
 * develop commercial activities involving the ExpressUI software without
 * disclosing the source code of your own applications. These activities
 * include: offering paid services to customers as an ASP, providing
 * services from a web application, shipping ExpressUI with a closed
 * source product.
 *
 * For more information, please contact Brown Bag Consulting at this
 * address: juan@brownbagconsulting.com.
 */

package com.expressui.core;

import com.expressui.core.entity.security.User;
import com.expressui.core.security.SecurityService;
import com.expressui.core.util.*;
import com.expressui.core.view.TypedComponent;
import com.expressui.core.view.ViewBean;
import com.expressui.core.view.field.LabelRegistry;
import com.expressui.core.view.menu.MainMenuBar;
import com.expressui.core.view.menu.MenuBarNode;
import com.expressui.core.view.page.Page;
import com.expressui.core.view.page.PageConversation;
import com.expressui.core.view.util.MessageSource;
import com.github.wolfie.sessionguard.SessionGuard;
import com.vaadin.Application;
import com.vaadin.terminal.ExternalResource;
import com.vaadin.terminal.ThemeResource;
import com.vaadin.terminal.gwt.server.HttpServletRequestListener;
import com.vaadin.terminal.gwt.server.WebApplicationContext;
import com.vaadin.ui.*;
import com.vaadin.ui.themes.ChameleonTheme;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.log4j.Logger;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.context.annotation.Scope;
import org.springframework.dao.DataIntegrityViolationException;
import org.vaadin.dialogs.ConfirmDialog;
import org.vaadin.dialogs.DefaultConfirmDialogFactory;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.persistence.EntityNotFoundException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * Main Vaadin Application, which is tied to the user's session. The user's MainApplication
 * is also tied to the current thread and can be looked up by calling getInstance().
 */
public abstract class MainApplication extends Application implements ViewBean, HttpServletRequestListener {

    private static ThreadLocal<HttpServletRequest> currentRequest = new ThreadLocal<HttpServletRequest>();

    private static ThreadLocal<HttpServletResponse> currentResponse = new ThreadLocal<HttpServletResponse>();

    private static ThreadLocal<MainApplication> currentInstance = new ThreadLocal<MainApplication>();

    private final Logger log = Logger.getLogger(getClass());

    /**
     * Application properties defined in application.properties
     */
    @Resource
    public ApplicationProperties applicationProperties;

    /**
     * Service for logging in/out and getting the current user. The current user entity
     * provides access to roles and permissions.
     */
    @Resource
    public SecurityService securityService;

    /**
     * Provides access to internationalized UI messages.
     */
    @Resource
    public MessageSource uiMessageSource;

    /**
     * Provides messages (display labels) associated with domain-level entities.
     */
    @Resource
    public MessageSource domainMessageSource;

    /**
     * Provides validation error messages.
     */
    @Resource
    public MessageSource validationMessageSource;

    /**
     * A registry for managing UI display labels.
     */
    @Resource
    public LabelRegistry labelRegistry;

    /**
     * Main menu bar displayed for navigation.
     */
    @Resource
    public MainMenuBar mainMenuBar;

    private TabSheet pageLayoutTabSheet;

    private Class<? extends Page> currentPageClass;
    private Class<? extends Page> previousPageClass;

    private PageConversation currentPageConversation;

    public abstract void configureLeftMenuBar(MenuBarNode rootNode);

    public abstract void configureRightMenuBar(MenuBarNode rootNode);

    /**
     * Gets the custom style theme for this application. Normally, applications would want to override this.
     * For example, a custom theme called "sample" would activate a style sheet found at
     * VAADIN/themes/sample/styles.css. Any custom theme should also import
     * ../chameleon/styles.css, ../chameleon-blue/styles.css, ../expressui/styles.css, as shown in sample app.
     *
     * @return name of custom theme
     */
    public String getCustomTheme() {
        return "expressui";
    }

    /**
     * Gets domain message associated with this component and given code.
     *
     * @param code code to prepend class name
     * @return internationalized domain message
     */
    public String getDomainMessage(String code) {
        return domainMessageSource.getMessage(getClass().getName() + "." + code);
    }

    /**
     * Gets domain message associated with this component and given code.
     *
     * @param code code to prepend class name
     * @param args used to interpolate the message
     * @return internationalized domain message
     */
    public String getDomainMessage(String code, Object... args) {
        return domainMessageSource.getMessage(getClass().getName() + "." + code, args);
    }

    /**
     * Gets the current page conversation.
     *
     * @return current page conversation
     */
    public PageConversation getCurrentPageConversation() {
        return currentPageConversation;
    }

    /**
     * Begins a new page conversation.
     *
     * @param id unique id of conversation
     * @return newly created page conversation
     */
    public PageConversation beginPageConversation(String id) {
        currentPageConversation = new PageConversation(id);

        return currentPageConversation;
    }

    /**
     * Ends the current page conversation. This doesn't need to be called, since a new page conversation automatically
     * ends the previous conversation.
     */
    public void endPageConversation() {
        currentPageConversation = null;
    }

    @PostConstruct
    @Override
    public void postConstruct() {
        currentInstance.set(this);
    }

    @Override
    public void postWire() {
    }

    @Override
    public void onDisplay() {
        mainMenuBar.onDisplay();
    }

    /**
     * Gets instance of MainApplication associated with the current HTTP session.
     * The user's MainApplication is always tied to the current thread and can be looked up by calling getInstance().
     *
     * @return MainApplication associated with user's session
     */
    public static MainApplication getInstance() {
        return currentInstance.get();
    }

    /**
     * Sets instance of MainApplication associated with the current HTTP session.
     * The user's MainApplication is always tied to the current thread and can be looked up by calling getInstance().
     *
     * @param mainApplication MainApplication associated with user's session
     */
    public static void setInstance(MainApplication mainApplication) {
        currentInstance.set(mainApplication);
    }

    /**
     * Gets the current HTTP servlet request.
     *
     * @return current HTTP service request
     */
    public static HttpServletRequest getRequest() {
        return currentRequest.get();
    }

    /**
     * Gets the current HTTP servlet response.
     *
     * @return current HTTP service response
     */
    public static HttpServletResponse getResponse() {
        return currentResponse.get();
    }

    @Override
    public void onRequestStart(HttpServletRequest request, HttpServletResponse response) {
        currentInstance.set(this);
        currentRequest.set(request);
        currentResponse.set(response);
        if (securityService.getCurrentUser() != null) {
            SecurityService.setCurrentLoginName(securityService.getCurrentUser().getLoginName());
        }
    }

    @Override
    public void onRequestEnd(HttpServletRequest request, HttpServletResponse response) {
        currentResponse.remove();
        currentRequest.remove();
        currentInstance.remove();
        SecurityService.removeCurrentLoginName();
    }

    /**
     * Gets cookie associated with given name.
     *
     * @param name name of the cookie
     * @return cookie
     */
    public Cookie getCookie(String name) {
        Cookie[] cookies = getRequest().getCookies();
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals(name)) {
                return cookie;
            }
        }

        return null;
    }

    /**
     * Adds a cookie to the HTTP response.
     *
     * @param name  name of the cookie
     * @param value value
     */
    public void addCookie(String name, String value) {
        Cookie cookie = new Cookie(name, value);
        cookie.setPath("/");
        getResponse().addCookie(cookie);
    }

    /**
     * Adds a cookie to the HTTP response.
     *
     * @param name   name of the cookie
     * @param value  value
     * @param maxAge max age
     * @see Cookie#Cookie(String, String)
     * @see Cookie#setMaxAge(int)
     */
    public void addCookie(String name, String value, int maxAge) {
        Cookie cookie = new Cookie(name, value);
        cookie.setMaxAge(maxAge);
        cookie.setPath("/");
        getResponse().addCookie(cookie);
    }

    /**
     * Adds a cookie to the HTTP response.
     *
     * @see Cookie
     */
    public void addCookie(Cookie cookie) {
        getResponse().addCookie(cookie);
    }

    @Override
    public void init() {
        currentInstance.set(this);

        setTheme(getCustomTheme());
        customizeConfirmDialogStyle();

        Window mainWindow = new Window();
        setMainWindow(mainWindow);
        mainWindow.addStyleName("e-main-window");
        mainWindow.setCaption(getTypeCaption());

        VerticalLayout mainLayout = new VerticalLayout();
        String id = StringUtil.generateDebugId("e", this, mainLayout, "mainLayout");
        mainLayout.setDebugId(id);

        mainWindow.setSizeFull();
        mainLayout.setSizeFull();
        mainWindow.setContent(mainLayout);

        setLogoutURL(getApplicationProperties().getRestartApplicationUrl());

        configureLeftMenuBar(mainMenuBar.getLeftMenuBarRoot());
        configureRightMenuBar(mainMenuBar.getRightMenuBarRoot());
        mainLayout.addComponent(mainMenuBar);

        pageLayoutTabSheet = new TabSheet();
        id = StringUtil.generateDebugId("e", this, pageLayoutTabSheet, "pageLayoutTabSheet");
        pageLayoutTabSheet.setDebugId(id);

        pageLayoutTabSheet.addStyleName("e-main-page-layout");
        pageLayoutTabSheet.setSizeFull();
        mainLayout.addComponent(pageLayoutTabSheet);
        mainLayout.setExpandRatio(pageLayoutTabSheet, 1.0f);

        Link expressUILink = new Link(uiMessageSource.getMessage("mainApplication.footerMessage"),
                new ExternalResource(uiMessageSource.getMessage("mainApplication.footerLink")));
        expressUILink.setTargetName("_blank");
        expressUILink.setIcon(new ThemeResource("../expressui/favicon.png"));
        expressUILink.setSizeUndefined();
        mainLayout.addComponent(expressUILink);
        mainLayout.setComponentAlignment(expressUILink, Alignment.TOP_CENTER);

        configureSessionTimeout(mainWindow);
        postWire();
        onDisplay();
    }

    /**
     * Gets the caption that describes the type of this component. Looks up the caption from domainMessages/,
     * using the class name of this component as the key, for example, com.expressui.sample.SampleApplication.
     *
     * @return caption that describes type of this component
     */
    public String getTypeCaption() {
        return domainMessageSource.getMessage(getClass().getName());
    }

    private void configureSessionTimeout(Window mainWindow) {
        ((WebApplicationContext) getContext()).getHttpSession()
                .setMaxInactiveInterval(getApplicationProperties().getSessionTimeout() * 60);

        SessionGuard sessionGuard = new SessionGuard();
        Integer timeoutWarningPeriod = getApplicationProperties().getSessionTimeoutWarning();
        sessionGuard.setTimeoutWarningPeriod(timeoutWarningPeriod);
        sessionGuard.setTimeoutWarningXHTML(uiMessageSource.getMessage("mainApplication.timeoutWarning",
                new Object[] { timeoutWarningPeriod }));
        mainWindow.addComponent(sessionGuard);
    }

    /**
     * Selects a new page based on page's class. Does nothing if user does not have permission to access the page.
     * If the previous page is {@link Page#SCOPE_PAGE}, then it is removed from session storage and the UI tabsheet.
     * If the previous page is WebApplicationContext.SESSION_SCOPE, then it is retained
     * and in the UI as a hidden component in an unselected tab.
     * </P>
     * {@link org.springframework.web.context.WebApplicationContext#SCOPE_SESSION} pages display more quickly at the
     * cost of using more memory.
     *
     * @param pageClass class of the new page to select
     */
    public void displayPage(Class<? extends Page> pageClass) {
        User currentUser = securityService.getCurrentUser();

        if (!currentUser.isViewAllowed(pageClass.getName())) {
            showError(
                    uiMessageSource.getMessage("mainApplication.notAllowed", new Object[] { pageClass.getName() }));
            return;
        }

        Page page = loadPageBean(pageClass);

        if (page instanceof TypedComponent && !((TypedComponent) page).isViewAllowed()) {
            showError(uiMessageSource.getMessage("mainApplication.notAllowed",
                    new Object[] { ((TypedComponent) page).getType().getName() }));
            return;
        }

        if (!ObjectUtil.isEqual(currentPageClass, pageClass)) {
            previousPageClass = currentPageClass;
            currentPageClass = pageClass;
        }

        Page previousPage = (Page) pageLayoutTabSheet.getSelectedTab();
        if (previousPage != null) {
            Scope scope = previousPage.getClass().getAnnotation(Scope.class);
            if (scope != null && scope.value().equals(Page.SCOPE_PAGE)) {
                pageLayoutTabSheet.removeComponent(previousPage);
            }
        }

        boolean componentFound = false;
        Iterator<Component> components = pageLayoutTabSheet.getComponentIterator();
        while (components.hasNext()) {
            Component component = components.next();
            if (page.equals(component)) {
                pageLayoutTabSheet.setSelectedTab(component);
                componentFound = true;
            }
        }

        if (!componentFound) {
            page.postWire();
            pageLayoutTabSheet.addTab(page);
            pageLayoutTabSheet.setSelectedTab(page);
        }

        page.onDisplay();
    }

    /**
     * Asks if user is allowed to view page of given type.
     *
     * @param pageClass class of type Page
     * @return true if user is allowed.
     */
    public boolean isPageViewAllowed(Class<? extends Page> pageClass) {

        User currentUser = securityService.getCurrentUser();
        Class genericType = ReflectionUtil.getGenericArgumentType(pageClass);
        return currentUser.isViewAllowed(pageClass.getName()) && currentUser.isViewAllowed(genericType.getName());
    }

    /**
     * Navigates to the previously selected page, sort of like using back button, except only keeps a history of one
     * page.
     */
    public void selectPreviousPage() {
        if (previousPageClass != null) {
            displayPage(previousPageClass);
        }
    }

    /**
     * Forces all page beans to be loaded. This is useful for admin managing security permissions. In this case,
     * all pages must be loaded so that security becomes aware of the components whose permissions
     * can be altered.
     */
    public void loadAllPageBeans() {
        Map<String, String> typeLabels = labelRegistry.getTypeLabels();
        Set<Class> classes = new HashSet<Class>();
        for (String type : typeLabels.keySet()) {
            try {
                Class clazz = Class.forName(type);
                classes.add(clazz);
            } catch (ClassNotFoundException e) {
                // ignore for menu bar labels that have NullCommand
            }
        }

        for (Class clazz : classes) {
            if (Page.class.isAssignableFrom(clazz)) {
                loadPageBean(clazz);
            }
        }
    }

    private Page loadPageBean(Class<? extends Page> pageClass) {
        beginPageConversation(pageClass.getName());

        return SpringApplicationContext.getBean(pageClass);
    }

    private void customizeConfirmDialogStyle() {
        ConfirmDialog.Factory confirmDialogFactory = new DefaultConfirmDialogFactory() {
            @Override
            public ConfirmDialog create(String caption, String message, String okCaption, String cancelCaption) {
                ConfirmDialog confirmDialog;
                confirmDialog = super.create(caption, message, okCaption, cancelCaption);
                confirmDialog.setStyleName(ChameleonTheme.WINDOW_OPAQUE);
                confirmDialog.getOkButton().addStyleName("small default");
                confirmDialog.getCancelButton().addStyleName("small default");

                return confirmDialog;
            }
        };
        ConfirmDialog.setFactory(confirmDialogFactory);
    }

    @Override
    public void terminalError(com.vaadin.terminal.Terminal.ErrorEvent event) {
        Throwable rootThrowable = event.getThrowable();
        if (rootThrowable == null)
            return;

        Exception cause;
        if ((cause = ExceptionUtil.findThrowableInChain(rootThrowable,
                DataIntegrityViolationException.class)) != null) {
            log.warn("Terminal error: ", rootThrowable);
            getMainWindow().showNotification(uiMessageSource.getMessage("mainApplication.dataConstraintViolation"),
                    cause.getMessage(), Window.Notification.TYPE_ERROR_MESSAGE);
        } else if ((cause = ExceptionUtil.findThrowableInChain(rootThrowable,
                ConstraintViolationException.class)) != null) {
            log.warn("Terminal error: ", rootThrowable);
            ConstraintViolationException violationException = (ConstraintViolationException) cause;
            getMainWindow().showNotification(uiMessageSource.getMessage("mainApplication.dataConstraintViolation"),
                    violationException.getMessage(), Window.Notification.TYPE_ERROR_MESSAGE);
        } else if ((cause = ExceptionUtil.findThrowableInChain(rootThrowable,
                EntityNotFoundException.class)) != null) {
            log.warn("Terminal error: ", rootThrowable);
            getMainWindow().showNotification(uiMessageSource.getMessage("mainApplication.entityNotFound"),
                    Window.Notification.TYPE_ERROR_MESSAGE);
        } else {
            super.terminalError(event);
            log.error("Terminal error: ", rootThrowable);
            openErrorWindow(rootThrowable);
        }
    }

    /**
     * Shows big error box to user.
     *
     * @param errorMessage message to display
     */
    public void showError(String errorMessage) {
        getMainWindow().showNotification(errorMessage, Window.Notification.TYPE_ERROR_MESSAGE);
    }

    /**
     * Shows big warning box to user.
     *
     * @param warningMessage message to display
     */
    public void showWarning(String warningMessage) {
        getMainWindow().showNotification(warningMessage, Window.Notification.TYPE_WARNING_MESSAGE);
    }

    /**
     * Shows message box to user.
     *
     * @param humanizedMessage message to display
     */
    public void showMessage(String humanizedMessage) {
        getMainWindow().showNotification(humanizedMessage, Window.Notification.TYPE_HUMANIZED_MESSAGE);
    }

    /**
     * Shows notification to user, more customizable that other show* methods.
     *
     * @param notification customized notification
     */
    public void showNotification(Window.Notification notification) {
        getMainWindow().showNotification(notification);
    }

    /**
     * Shows notification to user, more customizable that other show* methods.
     *
     * @param caption   the message to show
     * @param type      type of message
     * @param position  desired notification position
     * @param delayMsec desired delay in msec, -1 to require the user to click the message
     */
    public void showNotification(String caption, int type, int position, int delayMsec) {
        Window.Notification notification = new Window.Notification(caption, type);
        notification.setPosition(position);
        notification.setDelayMsec(delayMsec);
        getMainWindow().showNotification(notification);
    }

    /**
     * Shows message in tray area, bottom right with 2000 milliseconds delay.
     *
     * @param message message to display
     */
    public void showTrayMessage(String message) {
        showTrayMessage(2000, message);
    }

    /**
     * Shows message in tray area.
     *
     * @param delayMSec delay in milliseconds to display message
     * @param message   message to display
     */
    public void showTrayMessage(int delayMSec, String message) {
        Window.Notification notification = new Window.Notification(message,
                Window.Notification.TYPE_TRAY_NOTIFICATION);
        notification.setPosition(Window.Notification.POSITION_BOTTOM_RIGHT);
        notification.setDelayMsec(delayMSec);
        notification.setHtmlContentAllowed(true);
        getInstance().showNotification(notification);
    }

    /**
     * Shows a yes/no confirmation dialog box.
     *
     * @param listener listener to capture whether user chooses yes or no
     */
    public void showConfirmationDialog(ConfirmDialog.Listener listener) {
        ConfirmDialog.show(getMainWindow(), uiMessageSource.getMessage("mainApplication.confirmationCaption"),
                uiMessageSource.getMessage("mainApplication.confirmationPrompt"),
                uiMessageSource.getMessage("mainApplication.confirmationYes"),
                uiMessageSource.getMessage("mainApplication.confirmationNo"), listener);
    }

    /**
     * Configures various messages to embed {@link ApplicationProperties#restartApplicationUrl}
     * if anything goes wrong, for example if session expires, communication error, out of sync, etc.
     * <p/>
     * Vaadin automatically calls this method.
     *
     * @return configured SystemMessages
     */
    public static SystemMessages getSystemMessages() {
        String restartUrl = getApplicationProperties().getRestartApplicationUrl();
        CustomizedSystemMessages customizedSystemMessages = new CustomizedSystemMessages();
        customizedSystemMessages.setSessionExpiredURL(restartUrl);
        customizedSystemMessages.setCommunicationErrorURL(restartUrl);
        customizedSystemMessages.setOutOfSyncURL(restartUrl);
        return customizedSystemMessages;
    }

    private static ApplicationProperties getApplicationProperties() {
        return (ApplicationProperties) SpringApplicationContext.getBean("applicationProperties");
    }

    /**
     * Opens a separate error Window with the full stack trace of a throwable
     * and error box highlighting the root cause message.
     *
     * @param throwable full stack trace of this throwable is displayed in error Window
     */
    public void openErrorWindow(Throwable throwable) {
        showError(ExceptionUtils.getRootCauseMessage(throwable));

        String message = ExceptionUtils.getFullStackTrace(throwable);
        Window errorWindow = new Window(uiMessageSource.getMessage("mainApplication.errorWindowCaption"));
        errorWindow.addStyleName("opaque");
        VerticalLayout layout = (VerticalLayout) errorWindow.getContent();
        layout.setSpacing(true);
        layout.setWidth("100%");
        errorWindow.setWidth("100%");
        errorWindow.setModal(true);
        Label label = new Label(message);
        label.setContentMode(Label.CONTENT_PREFORMATTED);
        layout.addComponent(label);
        errorWindow.setClosable(true);
        errorWindow.setScrollable(true);
        getMainWindow().addWindow(errorWindow);
    }

    /**
     * Logs out of application, clear credentials and ends user session.
     */
    public void logout() {
        securityService.logout();
        close();
        invalidateSession();
    }

    private void invalidateSession() {
        WebApplicationContext context = (WebApplicationContext) getContext();
        HttpSession httpSession = context.getHttpSession();
        httpSession.invalidate();
    }

    /**
     * Checks if application has access to external Internet, if there is no firewall or proxy interference.
     *
     * @param testUrl      url to test
     * @param errorMessage error message if test fails
     */
    public void checkInternetConnectivity(String testUrl, String errorMessage) {
        try {
            UrlUtil.getContents(testUrl);
        } catch (Exception e) {
            getInstance().showError(errorMessage);
        }
    }
}