se.natusoft.osgi.aps.apsuseradminweb.vaadin.components.editors.UserEditor.java Source code

Java tutorial

Introduction

Here is the source code for se.natusoft.osgi.aps.apsuseradminweb.vaadin.components.editors.UserEditor.java

Source

/* 
 * 
 * PROJECT
 *     Name
 *         APS User Admin Web
 *     
 *     Code Version
 *         0.10.0
 *     
 *     Description
 *         This is an administration web for aps-simple-user-service that allows editing of roles and users.
 *         
 * COPYRIGHTS
 *     Copyright (C) 2012 by Natusoft AB All rights reserved.
 *     
 * LICENSE
 *     Apache 2.0 (Open Source)
 *     
 *     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.
 *     
 * AUTHORS
 *     Tommy Svensson (tommy@natusoft.se)
 *         Changes:
 *         2012-08-26: Created!
 *         
 */
package se.natusoft.osgi.aps.apsuseradminweb.vaadin.components.editors;

import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.data.util.IndexedContainer;
import com.vaadin.event.DataBoundTransferable;
import com.vaadin.event.ItemClickEvent;
import com.vaadin.event.dd.DragAndDropEvent;
import com.vaadin.event.dd.DropHandler;
import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion;
import com.vaadin.ui.*;
import se.natusoft.osgi.aps.api.auth.user.APSSimpleUserService;
import se.natusoft.osgi.aps.api.auth.user.APSSimpleUserServiceAdmin;
import se.natusoft.osgi.aps.api.auth.user.model.Role;
import se.natusoft.osgi.aps.api.auth.user.model.User;
import se.natusoft.osgi.aps.api.auth.user.model.UserAdmin;
import se.natusoft.osgi.aps.apsuseradminweb.vaadin.EditorIdentifier;
import se.natusoft.osgi.aps.apsuseradminweb.vaadin.components.EditorPanel;
import se.natusoft.osgi.aps.apsuseradminweb.vaadin.components.HelpText;
import se.natusoft.osgi.aps.apsuseradminweb.vaadin.css.CSS;

import java.util.Properties;

/**
 * This is a component that edits a user. It is reusable within a session. The user to edit is
 * passed in setUser() by EditUserComponentHandler and DeleteUserComponentHandler.
 */
public class UserEditor extends EditorPanel implements EditorIdentifier {
    //
    // Constants
    //

    /** Vaadin data container key for table column 'Property' in 'propertiesEditor' table. */
    private static final String USER_PROPS_KEY = "Property";

    /** Vaadin data container key for table column 'Value' in 'propertiesEditor' table. */
    private static final String USER_PROPS_VALUE = "Value";

    /** Vaadin data container key for table column 'Role id' in 'availableRoles' and 'selectedRoles' tables. */
    private static final String ROLE_ID = "Role id";

    /** Vaadin data container key for table column 'Role Description' in 'availableRoles' and 'selectedRoles' tables. */
    private static final String ROLE_DESC = "Role Description";

    //
    // Private Members
    //

    /** The APS user service to get and persist user changes with. */
    private APSSimpleUserServiceAdmin userServiceAdmin = null;

    /** The currently edited user. */
    private UserAdmin user = null;

    /** Id provider for propertiesEditor data container. */
    private int propId = 0;

    /** This is set to the id of the selected property in propertiesEditor and is used to delete the selected property. */
    private Object selectedPropertyId = null;

    // GUI Components

    /** The text field showing the userid of the edited user. It is only changeable when creating a new user. */
    private TextField userId = null;

    /** The current auth for the user. */
    private PasswordField authCurrent = null;

    /** new auth for the user. */
    private PasswordField authNewOne = null;

    /** new auth for the user. */
    private PasswordField authNewTwo = null;

    /** A Table with editable=true containing the users properties. */
    private Table propertiesEditor = null;

    /** Provides help text for the properties editor. */
    private HelpText propsHelpText = null;

    /** Pressing this button adds an empty property to the propertiesEditor. */
    private Button plusButton = null;

    /** This is only enabled when a specific property have been selected in propertiesEditor and will remove that property. */
    private Button minusButton = null;

    /**
     * This is a Table containing all available roles minus the roles of the current user. Roles are dragged and
     * dropped from this table to the 'selectedRoles' table to add a role to the user.
     */
    private Table availableRoles = null;

    /**
     * This is a table containing the current users roles. Roles are dragged and dropped from this table to the
     * 'availableRoles' table to remove a role from the user.
     */
    private Table selectedRoles = null;

    /** Help text for the role tables. */
    private HelpText roleHelptext = null;

    /** Saves the current changes one way or another (save changed, create new user, delete user) */
    private Button saveButton = null;

    /**
     * This is set to true by DeleteUserComponentHandler and in this case this editor is used for deleting a user.
     * In the case of this being 'true' all components exception save & cancel buttons are disabled. The caption of
     * the save button is also changed to "Delete".
     * <p/>
     * Please note that when this gets set to 'true' it is automatically restored to false in save() and cancel().
     */
    private boolean forDelete = false;

    //
    // Constructors
    //

    /**
     * Creates a new UserEditor instance.
     *
     * @param userServiceAdmin The user admin service for editing the user.
     */
    public UserEditor(APSSimpleUserServiceAdmin userServiceAdmin) {
        this.userServiceAdmin = userServiceAdmin;

        this.setStyleName(CSS.APS_EDITING_TEXT);

        VerticalLayout verticalLayout = new VerticalLayout();
        verticalLayout.setSpacing(true);
        verticalLayout.setMargin(true);
        verticalLayout.setStyleName(CSS.APS_EDITING_TEXT + " " + CSS.APS_CONTENT_PANEL);

        // User id & Auth
        {
            HorizontalLayout firstRowLayout = new HorizontalLayout();
            firstRowLayout.setSpacing(true);

            this.userId = new TextField("User id");
            this.userId.setColumns(30);
            this.userId.setImmediate(true);
            this.userId.setEnabled(true);

            firstRowLayout.addComponent(this.userId);

            this.authCurrent = new PasswordField("Current auth");
            this.authCurrent.setColumns(30);
            this.authCurrent.setImmediate(true);
            this.authCurrent.setEnabled(true);

            firstRowLayout.addComponent(this.authCurrent);

            verticalLayout.addComponent(firstRowLayout);

            HorizontalLayout secondRowLayout = new HorizontalLayout();
            secondRowLayout.setSpacing(true);

            this.authNewOne = new PasswordField("New auth");
            this.authNewOne.setColumns(30);
            this.authNewOne.setImmediate(true);
            this.authNewOne.setEnabled(true);

            secondRowLayout.addComponent(this.authNewOne);

            this.authNewTwo = new PasswordField("New auth confirm");
            this.authNewTwo.setColumns(30);
            this.authNewTwo.setImmediate(true);
            this.authNewTwo.setEnabled(true);

            secondRowLayout.addComponent(this.authNewTwo);

            verticalLayout.addComponent(secondRowLayout);
        }

        // User Properties
        {
            this.propertiesEditor = new Table();
            this.propertiesEditor.setSelectable(true);
            this.propertiesEditor.setCaption("User properties");
            this.propertiesEditor.setPageLength(8);
            this.propertiesEditor.setEditable(true);
            this.propertiesEditor.setSizeFull();
            this.propertiesEditor.setColumnExpandRatio(USER_PROPS_KEY, 0.3f);
            this.propertiesEditor.setColumnExpandRatio(USER_PROPS_VALUE, 0.7f);
            this.propertiesEditor.setTableFieldFactory(new EditFieldFactory());
            this.propertiesEditor.addListener(new ItemClickEvent.ItemClickListener() {
                @Override
                public void itemClick(ItemClickEvent event) {
                    selectDeselectProperty(event.getItemId());
                }
            });
            verticalLayout.addComponent(this.propertiesEditor);

            // Buttons + info row
            {
                HorizontalLayout plusMinusRow = new HorizontalLayout();
                plusMinusRow.setSpacing(true);

                this.plusButton = new Button("+");
                this.plusButton.addListener(new Button.ClickListener() {
                    @Override
                    public void buttonClick(Button.ClickEvent event) {
                        addProperty();
                    }
                });
                plusMinusRow.addComponent(this.plusButton);

                this.minusButton = new Button("-");
                this.minusButton.addListener(new Button.ClickListener() {
                    @Override
                    public void buttonClick(Button.ClickEvent event) {
                        deleteProperty();
                    }
                });
                this.minusButton.setEnabled(false);
                plusMinusRow.addComponent(this.minusButton);

                this.propsHelpText = new HelpText(
                        "Press + to add new property, select property and press - to delete.");
                plusMinusRow.addComponent(this.propsHelpText);

                verticalLayout.addComponent(plusMinusRow);
            }
        }

        // Roles
        {
            HorizontalLayout rolesLayout = new HorizontalLayout();
            rolesLayout.setSizeFull();

            // Available
            this.availableRoles = new Table("Available roles");
            this.availableRoles.setImmediate(true);
            this.availableRoles.setPageLength(10);
            this.availableRoles.setSortAscending(true);
            this.availableRoles.setSizeFull();
            this.availableRoles.setDragMode(Table.TableDragMode.ROW);
            this.availableRoles.setDropHandler(new DropHandler() {
                @Override
                public void drop(DragAndDropEvent event) {
                    DataBoundTransferable t = (DataBoundTransferable) event.getTransferable();
                    Object itemId = t.getItemId();
                    removeRole(itemId);
                }

                @Override
                public AcceptCriterion getAcceptCriterion() {
                    return new RoleAcceptCriterion(UserEditor.this.availableRoles);
                }
            });
            VerticalLayout availableRolesFrame = new VerticalLayout();
            availableRolesFrame.setMargin(false, true, false, false);
            availableRolesFrame.addComponent(this.availableRoles);
            rolesLayout.addComponent(availableRolesFrame);

            // Selected
            this.selectedRoles = new Table("Selected roles");
            this.selectedRoles.setImmediate(true);
            this.selectedRoles.setPageLength(10);
            this.selectedRoles.setSortAscending(true);
            this.selectedRoles.setSizeFull();
            this.selectedRoles.setDragMode(Table.TableDragMode.ROW);
            this.selectedRoles.setDropHandler(new DropHandler() {
                @Override
                public void drop(DragAndDropEvent event) {
                    DataBoundTransferable t = (DataBoundTransferable) event.getTransferable();
                    Object itemId = t.getItemId();
                    addRole(itemId);
                }

                @Override
                public AcceptCriterion getAcceptCriterion() {
                    return new RoleAcceptCriterion(UserEditor.this.selectedRoles);
                }
            });
            VerticalLayout selectedRolesFrame = new VerticalLayout();
            selectedRolesFrame.setMargin(false, false, false, true);
            selectedRolesFrame.addComponent(this.selectedRoles);
            rolesLayout.addComponent(selectedRolesFrame);

            rolesLayout.setExpandRatio(availableRolesFrame, 0.5f);
            rolesLayout.setExpandRatio(selectedRolesFrame, 0.5f);

            verticalLayout.addComponent(rolesLayout);

            this.roleHelptext = new HelpText("Drag and drop roles back and forth to set or remove a role.");
            verticalLayout.addComponent(this.roleHelptext);
        }

        // Save & Cancel
        {
            HorizontalLayout saveCancelLayout = new HorizontalLayout();
            saveCancelLayout.setSpacing(true);

            this.saveButton = new Button("Save");
            this.saveButton.addListener(new Button.ClickListener() {
                @Override
                public void buttonClick(Button.ClickEvent event) {
                    save();
                }
            });
            saveCancelLayout.addComponent(saveButton);

            Button cancelButton = new Button("Cancel");
            cancelButton.addListener(new Button.ClickListener() {
                @Override
                public void buttonClick(Button.ClickEvent event) {
                    cancel();
                }
            });
            saveCancelLayout.addComponent(cancelButton);

            verticalLayout.addComponent(saveCancelLayout);
        }

        setContent(verticalLayout);
    }

    //
    // Methods
    //

    /**
     * This gets called when the user selects a specific property in the propertiesEditor.
     * <p/>
     * Please note that this gets called on both 'select' and 'deselect'!
     *
     * @param propertyId The id of the selected property.
     */
    private void selectDeselectProperty(Object propertyId) {
        // I doubt that you will ever get here with a null propertyId, but to be 100% safe ...
        if (propertyId != null) {
            if (this.selectedPropertyId != null && propertyId.equals(this.selectedPropertyId)) {
                this.minusButton.setEnabled(false);
                this.selectedPropertyId = null;
            } else {
                this.selectedPropertyId = propertyId;
                this.minusButton.setEnabled(true);
            }
        } else {
            this.selectedPropertyId = null;
            this.minusButton.setEnabled(false);
        }
    }

    /**
     * This adds one new empty property to the propertiesEditor. This gets called by the 'plusButton' click event handler.
     */
    private void addProperty() {
        IndexedContainer ic = (IndexedContainer) this.propertiesEditor.getContainerDataSource();
        ic.addItem(this.propId++);
        this.propertiesEditor.refreshRowCache();
    }

    /**
     * This deletes the currently selected property in the 'propertiesEditor' if one is selected.
     * This gets called by the 'minusButton' click event handler.
     */
    private void deleteProperty() {
        // This should never be null if we get here, but what the heck lets be sure anyhow.
        if (this.selectedPropertyId != null) {
            IndexedContainer container = (IndexedContainer) this.propertiesEditor.getContainerDataSource();
            container.removeItem(this.selectedPropertyId);
            this.minusButton.setEnabled(false);
            this.selectedPropertyId = null;
        }
    }

    /**
     * Adds a role to the user. This is called by the DropHandler for 'selectedRoles' table when a new role
     * is dropped on it.
     *
     * @param itemId This identifies the item of the 'availableRoles' table that was dropped.
     */
    private void addRole(Object itemId) {
        if (itemId != null) {
            Item item = this.availableRoles.getItem(itemId);
            String roleId = (String) item.getItemProperty(ROLE_ID).getValue();
            String roleDesc = (String) item.getItemProperty(ROLE_DESC).getValue();
            this.availableRoles.removeItem(itemId);

            item = this.selectedRoles.addItem(roleId);
            item.getItemProperty(ROLE_ID).setValue(roleId);
            item.getItemProperty(ROLE_DESC).setValue(roleDesc);

            IndexedContainer selectedRolesContainer = (IndexedContainer) this.selectedRoles
                    .getContainerDataSource();
            selectedRolesContainer.sort(new Object[] { ROLE_ID }, new boolean[] { true });
        }
    }

    /**
     * Removes a role from the user. This is called by the DropHandler for 'availableRoles' table when a non contained
     * role is dropped on it from the 'selectedRoles' table. Please note that a role only exists in one or the other
     * of the two role tables.
     *
     * @param itemId This identifies the item of the 'selectedRoles' table that was dropped.
     */
    private void removeRole(Object itemId) {
        if (itemId == null) {
            itemId = this.selectedRoles.getValue();
        }

        if (itemId != null) {
            Item item = this.selectedRoles.getItem(itemId);
            String roleId = (String) item.getItemProperty(ROLE_ID).getValue();
            String roleDesc = (String) item.getItemProperty(ROLE_DESC).getValue();
            this.selectedRoles.removeItem(itemId);

            item = this.availableRoles.addItem(roleId);
            item.getItemProperty(ROLE_ID).setValue(roleId);
            item.getItemProperty(ROLE_DESC).setValue(roleDesc);

            IndexedContainer availRolesContainer = (IndexedContainer) this.availableRoles.getContainerDataSource();
            availRolesContainer.sort(new Object[] { ROLE_ID }, new boolean[] { true });

        }
    }

    /**
     * Saves the current changes. This only dispatches to doSave() or doDelete().
     */
    private void save() {
        try {
            if (this.forDelete) {
                doDelete();
            } else {
                doSave();
            }
        } catch (RuntimeException re) {
            notifyError("Failed to save user!", "Failed to save user: " + re.getMessage());
            getLogger().error("Failed to save user!", re);
            throw re;
        } finally {
            this.forDelete = false;
        }
    }

    /**
     * This does the actual saving of changed information.
     */
    private void doSave() {
        boolean newUser = false;
        if (this.user == null) {
            User checkUser = this.userServiceAdmin.getUser(this.userId.getValue().toString());
            if (checkUser != null) {
                notifyError("User already exists!",
                        "The user with id '" + this.userId.getValue().toString() + "' already exists!");
                // In this case we want the admin user to be able to correct his/her mistake and try saving again.
                // We should therefore not call 'refreshDependentComponents()'!
                return;
            } else {
                if (this.authNewOne.getValue() != null && this.authNewTwo.getValue() != null
                        && this.authNewOne.getValue().equals(this.authNewTwo.getValue())) {
                    this.user = this.userServiceAdmin.createUser(this.userId.getValue().toString());
                    this.userServiceAdmin.setUserAuthentication(this.user, this.authNewOne.getValue().toString());
                    newUser = true;
                } else {
                    notifyError("User not created!",
                            "Password was not specified for user or the two entered passwords " + "did not match!");
                    return;
                }

            }
        }

        // Properties
        Properties props = new Properties();
        IndexedContainer propsContainer = (IndexedContainer) this.propertiesEditor.getContainerDataSource();
        for (Object itemId : propsContainer.getItemIds()) {
            Item item = propsContainer.getItem(itemId);

            String propKey = item.getItemProperty(USER_PROPS_KEY).getValue().toString();
            String propValue = item.getItemProperty(USER_PROPS_VALUE).getValue().toString();

            if (propKey.trim().length() > 0 && propValue.trim().length() > 0) {
                props.put(propKey, propValue);
            }
        }
        this.user.setUserProperties(props);
        // Refresh the properties editor since empty lines can have been filtered.
        this.propertiesEditor.setContainerDataSource(createUserPropertiesContainer(this.user));

        // Roles
        IndexedContainer rolesContainer = (IndexedContainer) this.selectedRoles.getContainerDataSource();
        for (Object itemId : rolesContainer.getItemIds()) {
            Item item = rolesContainer.getItem(itemId);
            String roleId = item.getItemProperty(ROLE_ID).getValue().toString();

            if (!this.user.hasRole(roleId)) {
                Role role = this.userServiceAdmin.getRole(roleId);
                this.user.addRole(role);
            }
        }
        if (this.user.getRoles() != null) {
            for (Role role : this.user.getRoles()) {
                boolean roleStillValid = false;
                for (Object itemId : rolesContainer.getItemIds()) {
                    Item item = rolesContainer.getItem(itemId);
                    String roleId = item.getItemProperty(ROLE_ID).getValue().toString();
                    if (roleId.equals(role.getId())) {
                        roleStillValid = true;
                        break;
                    }
                }

                if (!roleStillValid) {
                    this.user.removeRole(role);
                }
            }
        }

        this.userServiceAdmin.updateUser(this.user);

        if (newUser) {
            notifySuccess("User created!", "Created new user '" + this.user.getId() + "'!");
        } else {
            if (this.authNewOne.getValue().toString().length() > 0) {
                if (this.userServiceAdmin.authenticateUser(this.user, this.authCurrent.getValue(),
                        APSSimpleUserService.AUTH_METHOD_PASSWORD)) {
                    if (this.authNewOne.getValue().equals(this.authNewTwo.getValue())) {
                        this.userServiceAdmin.setUserAuthentication(this.user,
                                this.authNewOne.getValue().toString());
                    } else {
                        notifyError("User partly saved!",
                                "User data saved, but failed to change auth due to the two entered new values "
                                        + "did not match!");
                    }
                } else {
                    notifyError("User partly saved!",
                            "User data saved, but failed to change auth due to entered current auth was "
                                    + "incorrect!");
                }
            } else {
                notifySuccess("User change saved!",
                        "User change " + (this.user != null ? "for '" + this.user.getId() + "' " : "") + "saved!");
            }
        }

        // Note that we only do refreshDependendComponents() here, not clearCenter()! This however does not matter.
        // The notify above pulls focus from the selected menu entry which triggers the menu selection action with a null
        // selection (unselect) which restores the description page. I currently see no workaround to this problem!
        refreshDependentComponents();

    }

    /**
     * This deletes the currently edited user.
     */
    private void doDelete() {
        if (this.user != null) {
            this.userServiceAdmin.deleteUser(this.user);

            notifySuccess("Deleted user!", "Deleted user '" + this.user.getId() + "'!");
            clearCenter();
            refreshDependentComponents();
        }
    }

    /**
     * This cancels the edit operation ignoring all changes made.
     */
    private void cancel() {
        this.forDelete = false;
        notifySuccess("User edit cancelled",
                "User change " + (this.user != null ? "for '" + this.user.getId() + "' " : "") + "cancelled!");
        clearCenter();
        refreshDependentComponents();
    }

    /**
     * Sets the user to work on. This is called by EditUserComponentHandler and DeleteUserComponentHandler.
     *
     * @param user The user to work on.
     */
    public void setUser(UserAdmin user) {
        this.user = user;
        enableComponents(!this.forDelete);
        configureComponents();

        this.propertiesEditor.setContainerDataSource(createUserPropertiesContainer(user));
        this.availableRoles.setContainerDataSource(createAvailableRolesContainer(user));
        this.selectedRoles.setContainerDataSource(createUserRolesContainer(user));
    }

    /**
     * This sets up this editor for deleting a user. This is called by DeleteUserComponentHandler. This will disable
     * all components other than the buttons at the bottom. This so that the user can still see all informatoin about
     * the user he/she is about to delete.
     *
     * @param forDelete true or false, but in reality always true when this is called.
     */
    public void setForDelete(boolean forDelete) {
        this.forDelete = forDelete;
        configureComponents();
        enableComponents(!forDelete);
    }

    /**
     * This configures some of this user editors components depending on the state of the editor (new user, existing user,
     * deleting user).
     */
    private void configureComponents() {
        if (this.user != null) {
            setCaption((this.forDelete ? "Deleting " : "Editing ") + "user '" + user.getId() + "'");
            this.userId.setValue(user.getId());
            this.userId.setEnabled(false);
            this.authCurrent.setValue("");
            this.authCurrent.setEnabled(true);
            this.authNewOne.setValue("");
            this.authNewTwo.setValue("");
            this.saveButton.setCaption(this.forDelete ? "Delete" : "Save");
        } else {
            setCaption("Editing new user");
            this.userId.setValue("");
            this.userId.setEnabled(true);
            this.authCurrent.setValue("");
            this.authCurrent.setEnabled(false);
            this.authNewOne.setValue("");
            this.authNewTwo.setValue("");
            this.saveButton.setCaption("Create");
        }
    }

    /**
     * Enables or disables certain components of this editor. This is called by setUser() and setForDelete(). It is physically
     * possible to call these 2 methods in any order and both affects the state of the components managed by this method, which
     * is why both call this to make sure it is in the correct state.
     *
     * @param enable true or false.
     */
    private void enableComponents(boolean enable) {
        this.propertiesEditor.setEnabled(enable);
        this.plusButton.setEnabled(enable);
        this.availableRoles.setEnabled(enable);
        this.selectedRoles.setEnabled(enable);
        this.propsHelpText.setEnabled(enable);
        this.roleHelptext.setEnabled(enable);
        this.authCurrent.setEnabled(enable);
        this.authNewOne.setEnabled(enable);
        this.authNewTwo.setEnabled(enable);
    }

    /**
     * Factory method to create a Vaadin IndexedContainer containing the users all properties. This is
     * used by the 'propertiesEditor' Table.
     *
     * @param user The user to get the properties from.
     *
     * @return The created container.
     */
    private IndexedContainer createUserPropertiesContainer(User user) {
        IndexedContainer userPropsContainer = new IndexedContainer();
        userPropsContainer.addContainerProperty(USER_PROPS_KEY, String.class, "");
        userPropsContainer.addContainerProperty(USER_PROPS_VALUE, String.class, "");

        this.propId = 0;
        if (user != null) {
            Properties props = user.getUserProperties();
            for (String propName : props.stringPropertyNames()) {
                String propValue = props.getProperty(propName);

                Item item = userPropsContainer.addItem(this.propId++);
                item.getItemProperty(USER_PROPS_KEY).setValue(propName);
                item.getItemProperty(USER_PROPS_VALUE).setValue(propValue);
            }
        }

        return userPropsContainer;
    }

    /**
     * Factory method to create a Vaadin IndexedContainer containing all available roles minus the specified
     * users all roles. This is used by the 'availableRoles' Table.
     *
     * @param user The user whose roles should be subtracted for all roles.
     *
     * @return The created container.
     */
    private IndexedContainer createAvailableRolesContainer(User user) {
        IndexedContainer availRolesContainer = new IndexedContainer();
        availRolesContainer.addContainerProperty(ROLE_ID, String.class, "");
        availRolesContainer.addContainerProperty(ROLE_DESC, String.class, "");
        for (Role role : this.userServiceAdmin.getRoles()) {
            if (role.isMasterRole()) {
                Item item = availRolesContainer.addItem(role.getId());
                item.getItemProperty(ROLE_ID).setValue(role.getId());
                item.getItemProperty(ROLE_DESC).setValue(role.getDescription());
            }
        }

        // Remove all roles already set on the user.
        if (user != null) {
            for (Role role : ((UserAdmin) user).getRoles()) {
                availRolesContainer.removeItem(role.getId());
            }
        }

        availRolesContainer.sort(new Object[] { ROLE_ID }, new boolean[] { true });

        return availRolesContainer;
    }

    /**
     * Factory method to create a Vaadin IndexedContainer containing the specified users all roles.
     * This is used by the 'selectedRoles' Table.
     *
     * @param user The user to get the roles from.
     *
     * @return The created container.
     */
    private IndexedContainer createUserRolesContainer(User user) {
        IndexedContainer userRolesContainer = new IndexedContainer();
        userRolesContainer.addContainerProperty(ROLE_ID, String.class, "");
        userRolesContainer.addContainerProperty(ROLE_DESC, String.class, "");

        if (user != null) {
            for (Role role : ((UserAdmin) user).getRoles()) {
                Item item = userRolesContainer.addItem(role.getId());
                item.getItemProperty(ROLE_ID).setValue(role.getId());
                item.getItemProperty(ROLE_DESC).setValue(role.getDescription());
            }
        }

        userRolesContainer.sort(new Object[] { ROLE_ID }, new boolean[] { true });

        return userRolesContainer;
    }

    /**
     * Implementation of EditorIdentifier.
     *
     * @return An id for this editor.
     */
    @Override
    public String getEditorId() {
        return this.user != null ? this.user.getId() : "";
    }

    //
    // Inner Classes
    //

    /**
     * This is a TableFieldFactory used on 'propertiesEditor' to produce TextFields that fill the whole
     * table column. The TextFields created by the default factory does not!
     */
    private static class EditFieldFactory implements TableFieldFactory {

        /**
         * Creates a field based on the Container, item id, property id and the
         * component responsible for displaying the field (most commonly
         * {@link com.vaadin.ui.Table}).
         *
         * @param container  the Container where the property belongs to.
         * @param itemId     the item Id.
         * @param propertyId the Id of the property.
         * @param uiContext  the component where the field is presented.
         * @return A field suitable for editing the specified data or null if the
         *         property should not be editable.
         */
        @Override
        public Field createField(Container container, Object itemId, Object propertyId, Component uiContext) {
            TextField tf = new TextField();
            tf.setWidth("100%");
            return tf;
        }
    }

    /**
     * This is the "accept criterion" used by both DropHandler for 'availableRoles' and 'selectedRoles'.
     * It accepts any drop of a value that does not already exists in the target. This is kind of redundant
     * since the contents of the two above mentioned Tables are (should be) unique in conjunction. A role
     * can only be in one or the other never both at the same time.
     * <p/>
     * Vaadin however refused to do any dropping at all without this (I tried to keep it on the client
     * side until dropped, but failed). I'm hoping to resolve this and remove this inner class.
     */
    private static class RoleAcceptCriterion extends ServerSideCriterion {
        //
        // Private Members
        //

        /** The Table this criterion is for. */
        private Table sourceTable = null;

        //
        // Constructors
        //

        /**
         * Creates a new RoleAcceptCriterion.
         *
         * @param sourceTable The Table using this criterion.
         */
        public RoleAcceptCriterion(Table sourceTable) {
            this.sourceTable = sourceTable;
        }

        //
        // Methods
        //

        /**
         * Validates the data in event to be appropriate for the
         * {@link com.vaadin.event.dd.DropHandler#drop(com.vaadin.event.dd.DragAndDropEvent)} method.
         * <p/>
         * Note that even if your criterion is validated on client side, you should
         * always validate the data on server side too.
         *
         * @param dragEvent
         * @return
         */
        @Override
        public boolean accept(DragAndDropEvent dragEvent) {
            DataBoundTransferable t = (DataBoundTransferable) dragEvent.getTransferable();
            Object itemId = t.getItemId();
            IndexedContainer container = (IndexedContainer) this.sourceTable.getContainerDataSource();
            return !container.containsId(itemId);
        }
    }
}