com.github.peholmst.springsecuritydemo.ui.CategoryBrowser.java Source code

Java tutorial

Introduction

Here is the source code for com.github.peholmst.springsecuritydemo.ui.CategoryBrowser.java

Source

/*
 * Copyright (c) 2010 The original author(s)
 *
 * 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 com.github.peholmst.springsecuritydemo.ui;

import java.util.LinkedList;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.security.access.AccessDeniedException;

import com.github.peholmst.springsecuritydemo.domain.Category;
import com.github.peholmst.springsecuritydemo.services.CategoryService;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.util.BeanItem;
import com.vaadin.terminal.ThemeResource;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.ui.DefaultFieldFactory;
import com.vaadin.ui.Field;
import com.vaadin.ui.Form;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Tree;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Window.Notification;

/**
 * This class implements the category browser. Features:
 * <ul>
 * <li>Localized user interface</li>
 * <li>Show categories in a tree</li>
 * <li>Add, edit and remove categories</li>
 * <li>Modify access control lists for categories</li>
 * <li>Show audit log for categories</li>
 * <li>Notifies listeners when the current category is changed</li>
 * </ul>
 * The actual category browser component is returned by {@link #getComponent()}.
 * 
 * @author Petter Holmstrm
 */
public final class CategoryBrowser {

    /*
     * Note: I'm sure there are nicer ways of implementing the category browser,
     * but at least the implementation is hidden inside this class and can be
     * changed without affecting the rest of the application.
     */

    /**
     * Listener interface to be implemented by classes that want to know when
     * the currently selected category is changed.
     * 
     * @see CategoryBrowser#addListener(CategorySelectionListener)
     * @see CategoryBrowser#removeListener(CategorySelectionListener)
     * 
     * @author Petter Holmstrm
     */
    public static interface CategorySelectionListener {
        /**
         * Notifies the listener that the currently selected category is
         * <code>newCategory</code>.
         * 
         * @param newCategory
         *            the selected category, or <code>null</code> if no category
         *            is selected at all.
         */
        public void selectedCategoryChanged(Category newCategory);
    }

    /**
     * Apache Commons logger for logging stuff.
     */
    protected final Log logger = LogFactory.getLog(getClass());

    private final CategoryService categoryService;

    private final I18nProvider i18nProvider;

    /**
     * Creates a new <code>CategoryBrowser</code>.
     * 
     * @param categoryService
     *            the category service to use for fetching and modifying
     *            categories (must not be <code>null</code>).
     * @param i18nProvider
     *            the i18n provider to use for fetching localized strings (must
     *            not be <code>null</code>).
     */
    public CategoryBrowser(CategoryService categoryService, I18nProvider i18nProvider) {
        assert categoryService != null : "categoryService must not be null";
        assert i18nProvider != null : "i18nProvider must not be null";
        this.categoryService = categoryService;
        this.i18nProvider = i18nProvider;
        createComponent();
    }

    /**
     * Gets the category service that should be used when retrieving or
     * modifying categories.
     * 
     * @return the category service (never <code>null</code>).
     */
    protected CategoryService getCategoryService() {
        return categoryService;
    }

    /**
     * Gets the i18n provider that should be used to fetch localized strings.
     * 
     * @return the i18n provider (never <code>null</code>).
     */
    protected I18nProvider getI18nProvider() {
        return i18nProvider;
    }

    /**
     * Gets the category browser component that can be used in user interfaces.
     * 
     * @return the category browser component (never <code>null</code>).
     */
    public Component getComponent() {
        return browserComponent;
    }

    private LinkedList<CategorySelectionListener> listeners;

    /**
     * Adds <code>listener</code> to the list of listeners to be notified when
     * the category selection is changed. If the listener is <code>null</code>,
     * nothing happens.
     * 
     * @param listener
     *            the listener to add.
     */
    public void addListener(CategorySelectionListener listener) {
        if (listener == null) {
            return;
        }
        if (listeners == null) {
            listeners = new LinkedList<CategorySelectionListener>();
        }
        listeners.add(listener);
    }

    /**
     * Removes <code>listener</code> from the list of listeners. If the listener
     * is <code>null</code> or was never added, nothing happens.
     * 
     * @param listener
     *            the listener to remove.
     */
    public void removeListener(CategorySelectionListener listener) {
        if (listener == null || listeners == null) {
            return;
        }
        listeners.remove(listener);
        if (listeners.isEmpty()) {
            listeners = null;
        }
    }

    private Button refreshButton;
    private Button addButton;
    private Button editButton;
    private Button deleteButton;
    private Button aclButton;
    private Button auditButton;
    private CategoryContainer categoryContainer;
    private Tree categoryTree;
    private VerticalLayout browserComponent;
    private CategoryForm categoryForm;

    private static String[] visibleCategoryFormPropertyes = { "name", "description" };

    /**
     * This inner class defines a form that can be used to edit existing
     * Categories and add new ones.
     */
    private final class CategoryForm {

        private static final long serialVersionUID = -3572755537037975014L;
        private Form form;
        private VerticalLayout layout;
        private Category category;
        private boolean isNew;

        public CategoryForm() {
            layout = new VerticalLayout();
            layout.setVisible(false);
            layout.setMargin(true);
        }

        @SuppressWarnings("serial")
        private void createForm() {
            layout.removeAllComponents();

            form = new Form();
            form.setImmediate(true);
            form.setCaption(getI18nProvider().getMessage("categories.form.caption"));
            form.setWriteThrough(false);
            form.setInvalidCommitted(false);

            form.setFormFieldFactory(new DefaultFieldFactory() {
                public Field createField(Item item, Object propertyId, Component uiContext) {
                    Field field = super.createField(item, propertyId, uiContext);
                    field.setWidth("100%");
                    if ("name".equals(propertyId)) {
                        field.setRequired(true);
                        field.setRequiredError(getI18nProvider().getMessage("categories.form.nameEmpty"));
                    }
                    return field;
                };
            });

            final HorizontalLayout buttons = new HorizontalLayout();
            buttons.setSpacing(true);

            final Button saveButton = new Button("Save", new Button.ClickListener() {

                @Override
                public void buttonClick(ClickEvent event) {
                    /*
                     * First, we have to commit the form. This will validate
                     * the data and update the underlying category instance
                     * upon success.
                     */
                    try {
                        form.commit();
                    } catch (Exception e) {
                        /*
                         * The form contains errors, so we abort here and
                         * let the user fix them. The form will take care of
                         * displaying the error messages to the user.
                         */
                        return;
                    }
                    /*
                     * We now have a category instance with the updated
                     * data, so now we have to save it using the category
                     * service.
                     */
                    try {
                        if (isNew) {
                            category = getCategoryService().insertCategory(category);
                        } else {
                            category = getCategoryService().updateCategory(category);
                        }

                        /*
                         * Remember to refresh the container, otherwise the
                         * new/updated category won't show up in the tree.
                         */
                        categoryContainer.refresh();
                        /*
                         * Update selection, will update the enablement
                         * state
                         */
                        categoryTree.setValue(category.getId());

                        setVisible(false);
                        getComponent().getWindow().showNotification(
                                getI18nProvider().getMessage("categories.categorySaved"),
                                Notification.TYPE_TRAY_NOTIFICATION);
                    } catch (AccessDeniedException e) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Access denied while attempting to save category [" + category + "]", e);
                        }
                        if (isNew) {
                            ExceptionUtils.handleException(getComponent().getWindow(), e,
                                    getI18nProvider().getMessage("categories.add.accessDenied.descr"));
                        } else {
                            ExceptionUtils.handleException(getComponent().getWindow(), e,
                                    getI18nProvider().getMessage("categories.save.accessDenied.descr"));
                        }
                    } catch (OptimisticLockingFailureException e) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Optimistic locking failure while attempting to save category [" + category
                                    + "]", e);
                        }
                        ExceptionUtils.handleException(getComponent().getWindow(), e,
                                getI18nProvider().getMessage("categories.save.optLockFail.descr"));
                    } catch (Exception e) {
                        if (logger.isErrorEnabled()) {
                            logger.error("Error while attempting to save category [" + category + "]", e);
                        }
                        ExceptionUtils.handleException(getComponent().getWindow(), e);
                    }
                    /*
                     * Data integrity violation should never occur.
                     */
                }
            });
            saveButton.setStyleName("primary");
            buttons.addComponent(saveButton);

            final Button cancelButton = new Button("Cancel", new Button.ClickListener() {

                @Override
                public void buttonClick(ClickEvent event) {
                    form.discard();
                    setVisible(false);
                }
            });
            buttons.addComponent(cancelButton);

            layout.addComponent(form);
            layout.addComponent(buttons);
            layout.setExpandRatio(form, 1.0f);
            layout.setComponentAlignment(buttons, Alignment.BOTTOM_RIGHT);
        }

        /**
         * Gets the category form component that can be used in user interfaces.
         * 
         * @return a layout containing the form and buttons.
         */
        public Component getComponent() {
            return layout;
        }

        /**
         * Shows the form and binds it to <code>category</code>.
         * 
         * @param category
         *            the category to edit.
         */
        public void editCategory(Category category) {
            createForm();
            this.category = category;
            isNew = false;
            form.setItemDataSource(new BeanItem<Category>(category));
            form.setVisibleItemProperties(visibleCategoryFormPropertyes);
            setVisible(true);
        }

        /**
         * Shows the form and binds it to a newly created category.
         * 
         * @param parent
         *            the parent of the category, <code>null</code> to make a
         *            new root category.
         */
        public void newCategory(Category parent) {
            editCategory(new Category());
            this.category.setParent(parent);
            isNew = true;
        }

        /**
         * Shows or hides the form.
         * 
         * @param visible
         *            <code>true</code> to show the form, <code>false</code> to
         *            hide it.
         */
        public void setVisible(boolean visible) {
            getComponent().setVisible(visible);
        }
    }

    /**
     * Refreshes the category browser.
     */
    private void actionRefresh() {
        try {
            if (logger.isDebugEnabled()) {
                logger.debug("Attempting to refresh the category browser");
            }
            categoryContainer.refresh();
            updateEnablementState();
            getComponent().getWindow().showNotification(getI18nProvider().getMessage("categories.browserRefreshed"),
                    Notification.TYPE_TRAY_NOTIFICATION);
        } catch (Exception e) {
            if (logger.isErrorEnabled()) {
                logger.error("Error while attempting to refresh the category browser", e);
            }
            ExceptionUtils.handleException(getComponent().getWindow(), e);
        }
    }

    /**
     * Edits the selected category. If no category is selected, this method does
     * nothing.
     */
    private void actionEdit() {
        final Long selectedCategoryId = (Long) categoryTree.getValue();
        if (selectedCategoryId != null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Attempting to show edit form for category '" + selectedCategoryId + "'");
            }
            BeanItem<Category> categoryItem = categoryContainer.getItem(selectedCategoryId);
            if (categoryItem != null) {
                categoryForm.editCategory(categoryItem.getBean());
            } else {
                actionRefresh();
            }
        }
    }

    /**
     * Adds a new sub category to the selected category. If no category is
     * selected, a new root category is created.
     */
    private void actionAdd() {
        final Long selectedCategoryId = (Long) categoryTree.getValue();
        if (logger.isDebugEnabled()) {
            logger.debug("Attempting to show edit form for a new category");
        }
        if (selectedCategoryId == null) {
            // new root item
            categoryForm.newCategory(null);
        } else {
            BeanItem<Category> categoryItem = categoryContainer.getItem(selectedCategoryId);
            if (categoryItem != null) {
                categoryForm.newCategory(categoryItem.getBean());
            } else {
                actionRefresh();
            }
        }
    }

    /**
     * Deletes the selected category. If no category is selected, this method
     * does nothing.
     */
    private void actionDelete() {
        final Long selectedCategoryId = (Long) categoryTree.getValue();
        if (selectedCategoryId != null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Attempting to delete category '" + selectedCategoryId + "'");
            }
            try {
                Category c = getCategoryService().getCategoryById(selectedCategoryId);
                if (c != null) {
                    getCategoryService().deleteCategory(c);
                }
                /*
                 * Remember to refresh the container, otherwise the old category
                 * will remain in the tree.
                 */
                categoryContainer.refresh();
                /*
                 * Clear selection, will update the enablement state
                 */
                categoryTree.setValue(null);
                getComponent().getWindow().showNotification(
                        getI18nProvider().getMessage("categories.categoryDeleted"),
                        Notification.TYPE_TRAY_NOTIFICATION);
            } catch (AccessDeniedException e) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Access denied while attempting to delete category '" + selectedCategoryId + "'",
                            e);
                }
                ExceptionUtils.handleException(getComponent().getWindow(), e,
                        getI18nProvider().getMessage("categories.delete.accessDenied.descr"));
            } catch (OptimisticLockingFailureException e) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Optimistic locking failure while attempting to delete category '"
                            + selectedCategoryId + "'", e);
                }
                ExceptionUtils.handleException(getComponent().getWindow(), e,
                        getI18nProvider().getMessage("categories.delete.optLockFail.descr"));
            } catch (Exception e) {
                if (logger.isErrorEnabled()) {
                    logger.error("Error while attempting to delete category '" + selectedCategoryId + "'", e);
                }
                ExceptionUtils.handleException(getComponent().getWindow(), e);
            }
            /*
             * Data integrity violation should never occur.
             */
        }
    }

    /**
     * Shows the access control list for the selected category. If no category
     * is selected, this method does nothing.
     */
    private void actionACL() {
        // TODO Implement me!
        getComponent().getWindow().showNotification("Not implemented yet!");
    }

    /**
     * Shows the audit log for the selected category. If no category is
     * selected, this method does nothing.
     */
    private void actionAudit() {
        // TODO Implement me!
        getComponent().getWindow().showNotification("Not implemented yet!");
    }

    /**
     * Updates the enablement state of the toolbar buttons and hides the
     * category form.
     */
    private void updateEnablementState() {
        // Get selection
        boolean hasSelection = categoryTree.getValue() != null;
        /*
         * In a real application, security checks should be included here as
         * well in order to disable functions that the user is not allowed to
         * perform. In this demo, it is possible to try to perform illegal
         * operations just to demonstrate that the security features are
         * working.
         */
        editButton.setEnabled(hasSelection);
        deleteButton.setEnabled(hasSelection);
        aclButton.setEnabled(hasSelection);
        auditButton.setEnabled(hasSelection);
        categoryForm.setVisible(false);
    }

    /**
     * Creates the category browser component.
     */
    @SuppressWarnings("serial")
    private void createComponent() {
        categoryContainer = new CategoryContainer(getCategoryService());
        categoryContainer.refresh();

        /*
         * The tree will show all the categories returned from the category
         * service.
         */
        categoryTree = new Tree();
        {
            categoryTree.setSizeFull();
            categoryTree.setContainerDataSource(categoryContainer);
            categoryTree.setItemCaptionPropertyId("name");
            categoryTree.setImmediate(true);
        }

        /*
         * The form for editing categories is hidden by default and is shown
         * when the user clicks the edit or add button.
         */
        categoryForm = new CategoryForm();

        /*
         * The toolbar will be placed at the bottom of the browser and contains
         * buttons for refreshing the category tree, and adding, editing and
         * removing categories.
         */
        final HorizontalLayout toolbar = new HorizontalLayout();
        {
            /*
             * Button: Refresh the category browser
             */
            refreshButton = new Button();
            refreshButton.setIcon(new ThemeResource("icons/16/refresh.png"));
            refreshButton.setStyleName("small");
            refreshButton.setDescription(getI18nProvider().getMessage("categories.refresh.descr"));
            refreshButton.addListener(new Button.ClickListener() {

                @Override
                public void buttonClick(ClickEvent event) {
                    actionRefresh();
                }
            });
            toolbar.addComponent(refreshButton);

            /*
             * Button: Add a new category. The currently selected category will
             * be used as parent.
             */
            addButton = new Button();
            addButton.setIcon(new ThemeResource("icons/16/add.png"));
            addButton.setStyleName("small");
            addButton.setDescription(getI18nProvider().getMessage("categories.add.descr"));
            addButton.addListener(new Button.ClickListener() {

                @Override
                public void buttonClick(ClickEvent event) {
                    actionAdd();
                }
            });
            toolbar.addComponent(addButton);

            /*
             * Button: Edit the selected category
             */
            editButton = new Button();
            editButton.setIcon(new ThemeResource("icons/16/pencil.png"));
            editButton.setStyleName("small");
            editButton.setDescription(getI18nProvider().getMessage("categories.edit.descr"));
            editButton.setEnabled(false);
            editButton.addListener(new Button.ClickListener() {

                @Override
                public void buttonClick(ClickEvent event) {
                    actionEdit();
                }
            });
            toolbar.addComponent(editButton);

            /*
             * Button: Delete the selected category
             */
            deleteButton = new Button();
            deleteButton.setIcon(new ThemeResource("icons/16/delete.png"));
            deleteButton.setStyleName("small");
            deleteButton.setDescription(getI18nProvider().getMessage("categories.delete.descr"));
            deleteButton.setEnabled(false);
            deleteButton.addListener(new Button.ClickListener() {

                @Override
                public void buttonClick(ClickEvent event) {
                    actionDelete();
                }
            });
            toolbar.addComponent(deleteButton);

            /*
             * Button: Show/edit the access control list for the selected
             * category
             */
            aclButton = new Button();
            aclButton.setIcon(new ThemeResource("icons/16/lock_edit.png"));
            aclButton.setStyleName("small");
            aclButton.setDescription(getI18nProvider().getMessage("categories.acl.descr"));
            aclButton.setEnabled(false);
            aclButton.addListener(new Button.ClickListener() {

                @Override
                public void buttonClick(ClickEvent event) {
                    actionACL();
                }
            });
            toolbar.addComponent(aclButton);

            /*
             * Button: Show the audit log for the selected category
             */
            auditButton = new Button();
            auditButton.setIcon(new ThemeResource("icons/16/key.png"));
            auditButton.setStyleName("small");
            auditButton.setDescription(getI18nProvider().getMessage("categories.audit.descr"));
            auditButton.setEnabled(false);
            auditButton.addListener(new Button.ClickListener() {

                @Override
                public void buttonClick(ClickEvent event) {
                    actionAudit();
                }
            });
            toolbar.addComponent(auditButton);
        }

        /*
         * The browser layout contains the category tree and the toolbar.
         */
        browserComponent = new VerticalLayout();
        browserComponent.setSizeFull();
        browserComponent.addComponent(categoryTree);
        browserComponent.addComponent(categoryForm.getComponent());
        browserComponent.addComponent(toolbar);
        browserComponent.setExpandRatio(categoryTree, 1.0f);
        browserComponent.setComponentAlignment(toolbar, Alignment.BOTTOM_CENTER);

        /*
         * Register a listener that updates the enablement state every time the
         * selection changes.
         */
        categoryTree.addListener(new Property.ValueChangeListener() {

            @Override
            public void valueChange(ValueChangeEvent event) {
                updateEnablementState();
                Long newCategoryId = (Long) event.getProperty().getValue();
                Category newCategory = null;
                if (newCategoryId != null) {
                    BeanItem<Category> item = categoryContainer.getItem(newCategoryId);
                    if (item != null) {
                        newCategory = item.getBean();
                    }
                }
                fireCategorySelectionChanged(newCategory);
            }
        });
    }

    @SuppressWarnings("unchecked")
    private void fireCategorySelectionChanged(Category newCategory) {
        if (listeners == null) {
            return;
        }
        /*
         * Iterate over a cloned list instead of the list itself to avoid
         * strange behavior if any of the listeners add additional listeners or
         * remove existing ones.
         */
        LinkedList<CategorySelectionListener> clonedList = (LinkedList<CategorySelectionListener>) listeners
                .clone();
        for (CategorySelectionListener listener : clonedList) {
            listener.selectedCategoryChanged(newCategory);
        }

    }

}