org.eclipse.skalli.view.internal.window.ProjectEditPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.skalli.view.internal.window.ProjectEditPanel.java

Source

/*******************************************************************************
 * Copyright (c) 2010-2014 SAP AG and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     SAP AG - initial API and implementation
 *******************************************************************************/
package org.eclipse.skalli.view.internal.window;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.lang.StringUtils;
import org.eclipse.skalli.model.EntityBase;
import org.eclipse.skalli.model.ExtensionEntityBase;
import org.eclipse.skalli.model.Issue;
import org.eclipse.skalli.model.Project;
import org.eclipse.skalli.model.Severity;
import org.eclipse.skalli.model.User;
import org.eclipse.skalli.model.ValidationException;
import org.eclipse.skalli.services.Services;
import org.eclipse.skalli.services.entity.EntityServices;
import org.eclipse.skalli.services.extension.ExtensionService;
import org.eclipse.skalli.services.extension.ExtensionServices;
import org.eclipse.skalli.services.group.GroupUtils;
import org.eclipse.skalli.services.issues.Issues;
import org.eclipse.skalli.services.issues.IssuesService;
import org.eclipse.skalli.services.project.ProjectService;
import org.eclipse.skalli.services.template.ProjectTemplate;
import org.eclipse.skalli.services.template.ProjectTemplateService;
import org.eclipse.skalli.services.user.UserServices;
import org.eclipse.skalli.view.ext.ExtensionFormService;
import org.eclipse.skalli.view.ext.Navigator;
import org.eclipse.skalli.view.ext.ProjectEditContext;
import org.eclipse.skalli.view.ext.ProjectEditMode;
import org.eclipse.skalli.view.internal.application.ProjectApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.terminal.ThemeResource;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.CssLayout;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.Panel;
import com.vaadin.ui.ProgressIndicator;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.Window;

public class ProjectEditPanel extends Panel implements ProjectPanel {

    private static final long serialVersionUID = 2377962084815410728L;

    private static final Logger LOG = LoggerFactory.getLogger(ProjectEditPanel.class);

    private static final String STYLE_EDIT_PROJECT = "prjedt"; //$NON-NLS-1$
    private static final String STYLE_EDIT_PROJECT_LAYOUT = "prjedt-layout"; //$NON-NLS-1$
    private static final String STYLE_EDIT_PROJECT_BUTTONS = "prjedt-buttons"; //$NON-NLS-1$
    private static final String STYLE_EDIT_PROJECT_ERROR = "prjedt-errorLabel"; //$NON-NLS-1$
    private static final String STYLE_ISSUES = "prjedt-issues"; //$NON-NLS-1$

    private static final String HEADER = "header_"; //$NON-NLS-1$
    private static final String FOOTER = "footer_"; //$NON-NLS-1$
    private static final String BUTTON_OK = "button_ok"; //$NON-NLS-1$
    private static final String BUTTON_CANCEL = "button_cancel"; //$NON-NLS-1$
    private static final String BUTTON_EXPAND_ALL = "button_expand_all"; //$NON-NLS-1$
    private static final String BUTTON_COLLAPSE_ALL = "button_collapse_all"; //$NON-NLS-1$
    private static final String BUTTON_VALIDATE = "button_validate"; //$NON-NLS-1$

    private static final String PANEL_WIDTH = "600px"; //$NON-NLS-1$
    private static final String CONFIRM_POPUP_WIDTH = "350px"; //$NON-NLS-1$

    private final ThemeResource ICON_BUTTON_OK = new ThemeResource("icons/button/ok.png"); //$NON-NLS-1$
    private final ThemeResource ICON_BUTTON_CANCEL = new ThemeResource("icons/button/cancel.png"); //$NON-NLS-1$
    private final ThemeResource ICON_BUTTON_EXPAND_ALL = new ThemeResource("icons/button/openall.png"); //$NON-NLS-1$
    private final ThemeResource ICON_BUTTON_COLLAPSE_ALL = new ThemeResource("icons/button/closeall.png"); //$NON-NLS-1$
    private final ThemeResource ICON_BUTTON_VALIDATE = new ThemeResource("icons/button/validate.png"); //$NON-NLS-1$

    private static final String WARN_EXTENSION_DISABLED = "Extension <i>{0}</i> has been disabled";
    private static final String WARN_EXTENSION_INHERITED = "Extension <i>{0}</i> is inherited from parent project <i>{1}</i>";

    private Project project;
    private ProjectTemplate projectTemplate;
    private ProjectEditMode mode;

    private Project modifiedProject;

    private Map<String, ProjectEditPanelEntry> panels;
    private SortedSet<ProjectEditPanelEntry> sortedPanels;
    private Set<String> selectableExtensions;
    private Map<String, String> displayNames;

    private ProjectApplication application;
    private Navigator navigator;

    private Label headerLabel;
    private Label footerLabel;
    private Button headerCheckButton;
    private Button footerCheckButton;

    private CssLayout indicatorArea;
    private ProgressIndicator progressIndicator;
    private ProgressThread progressThread;
    private ValidatorThread validatorThread;

    private Issues persistedIssues;

    /**
     * Creates a project edit view for
     * <ul>
     *   <li>browsing of an existing project in readonly mode ({@link ProjectEditMode#VIEW_PROJECT})</li>
     *   <li>editing of an existing project ({@link ProjectEditMode#EDIT_PROJECT})</li>
     *   <li>editing of a new project ({@link ProjectEditMode#NEW_PROJECT})</li>
     * </ul>
     *
     * @param application  the application.
     * @param navigator  the navigator used to leave the dialog.
     * @param project  the project to browse or modify, or a freshly created project.
     * (see {@link ProjectService#createProject(String, String)}).
     * @param mode  one of {@link ProjectEditMode#VIEW_PROJECT}, {@link ProjectEditMode#EDIT_PROJECT} or
     * {@link ProjectEditMode#NEW_PROJECT}.
     */
    public ProjectEditPanel(ProjectApplication application, Navigator navigator, Project project,
            ProjectEditMode mode) {
        this.application = application;
        this.navigator = navigator;
        this.mode = mode;
        this.project = project;

        // If the project has been persisted before, do not modify the
        // cached project instance, but modify a copy loaded directly from storage.
        if (ProjectEditMode.NEW_PROJECT.equals(mode)) {
            modifiedProject = project;
        } else {
            ProjectService projectService = ((ProjectService) EntityServices.getByEntityClass(Project.class));
            modifiedProject = projectService.loadEntity(Project.class, project.getUuid());
        }

        ProjectTemplateService templateService = Services.getRequiredService(ProjectTemplateService.class);
        projectTemplate = templateService.getProjectTemplateById(project.getProjectTemplateId());

        initializeSelectableExtensions();
        loadPersistedIssues();
        initializePanelEntries();

        setSizeFull();
        setStyleName(STYLE_EDIT_PROJECT);
        renderContent((VerticalLayout) getContent());
    }

    @Override
    public Project getProject() {
        return project;
    }

    /**
     * Loads persisted issues for the project.
     */
    private void loadPersistedIssues() {
        if (modifiedProject.getUuid() == null) {
            LOG.info("New project, no issues available. Skipping loading of issues.");
            return;
        }

        IssuesService issuesService = Services.getService(IssuesService.class);
        if (issuesService == null) {
            LOG.warn("No issue service available. Skipping loading of issues.");
            return;
        }

        persistedIssues = issuesService.loadEntity(Issues.class, modifiedProject.getUuid());
    }

    /**
     * Initializes the {@link #selectableExtensions} field:
     * Calls {@link ProjectTemplateService#getSelectableExtensions(ProjectTemplate, Project)} to determine
     * all extensions that
     * <ul>
     * <li>can work with the given template (see {@link ExtensionService#getProjectTemplateIdservice()})</li>
     * <li>are not in the template's exclude list (if an exclude list is specified,
     * see {@link ProjectTemplate#getExcludedExtensions()})</li>
     * <li>are in the templates include list (if an include list is specified,
     * see {@link ProjectTemplate#getIncludedExtensions()})</li>
     * </ul>
     * If all checks succeed, the extension's class name is added to <code>selectableExtensions</code>.
     */
    private void initializeSelectableExtensions() {
        ProjectTemplateService projectTemplateService = Services.getRequiredService(ProjectTemplateService.class);
        selectableExtensions = new HashSet<String>();
        for (Class<? extends ExtensionEntityBase> extension : projectTemplateService
                .getSelectableExtensions(projectTemplate, modifiedProject)) {
            selectableExtensions.add(extension.getName());
        }
    }

    /**
     * Creates and initializes the panels.
     * Iterates over all available {@link ExtensionFormService} and creates a {@link ProjectEditPanelEntry}
     * for each form service that is supported by the project template of the project to edit.
     */
    @SuppressWarnings("rawtypes")
    private void initializePanelEntries() {
        panels = new HashMap<String, ProjectEditPanelEntry>();
        sortedPanels = new TreeSet<ProjectEditPanelEntry>(new ProjectEditPanelComparator(projectTemplate));
        displayNames = new HashMap<String, String>();
        Context context = new Context(application, projectTemplate, mode);
        Iterator<ExtensionFormService> extensionFormFactories = Services
                .getServiceIterator(ExtensionFormService.class);
        while (extensionFormFactories.hasNext()) {
            ExtensionFormService extensionFormFactory = extensionFormFactories.next();
            String extensionClassName = extensionFormFactory.getExtensionClass().getName();
            if (selectableExtensions.contains(extensionClassName)) {
                ProjectEditPanelEntry entry = new ProjectEditPanelEntry(modifiedProject, extensionFormFactory,
                        context, application);
                entry.setWidth(PANEL_WIDTH);
                panels.put(extensionClassName, entry);
                sortedPanels.add(entry);
                displayNames.put(extensionClassName, entry.getDisplayName());
            }
        }
    }

    void renderContent(VerticalLayout content) {
        content.setStyleName(STYLE_EDIT_PROJECT_LAYOUT);
        headerCheckButton = renderButtons(content, true);
        renderProgessIndicator(content);
        headerLabel = renderMessageArea(content);
        renderPanels(content);
        footerLabel = renderMessageArea(content);
        footerCheckButton = renderButtons(content, false);
        renderPersistedIssues();
    }

    private void renderProgessIndicator(VerticalLayout layout) {
        indicatorArea = new CssLayout();
        indicatorArea.setVisible(false);
        indicatorArea.setMargin(true);
        indicatorArea.setWidth(PANEL_WIDTH);
        indicatorArea
                .addComponent(new Label("<strong>Project is checked for issues</strong>", Label.CONTENT_XHTML));
        progressIndicator = new ProgressIndicator();
        progressIndicator.setWidth("300px");
        progressIndicator.setIndeterminate(false);
        indicatorArea.addComponent(progressIndicator);
        layout.addComponent(indicatorArea);
        layout.setComponentAlignment(indicatorArea, Alignment.MIDDLE_CENTER);
    }

    private Label renderMessageArea(VerticalLayout layout) {
        CssLayout messageArea = new CssLayout();
        messageArea.setMargin(true);
        messageArea.setWidth(PANEL_WIDTH);
        Label label = new Label("", Label.CONTENT_XHTML); //$NON-NLS-1$
        label.addStyleName(STYLE_ISSUES);
        label.setVisible(false);
        messageArea.addComponent(label);
        layout.addComponent(messageArea);
        layout.setComponentAlignment(messageArea, Alignment.MIDDLE_CENTER);
        return label;
    }

    private void setMessage(Label label, String message) {
        if (StringUtils.isNotEmpty(message)) {
            label.setValue(message);
            label.setVisible(true);
        } else {
            label.setVisible(false);
        }
        label.requestRepaint();
    }

    private void setMessage(String message) {
        setMessage(headerLabel, message);
        setMessage(footerLabel, message);
    }

    private void setMessage(SortedSet<Issue> issues, Map<String, String> displayNames) {
        String message = Issue.asHTMLList(null, issues, displayNames);
        setMessage(headerLabel, message);
        setMessage(footerLabel, message);
    }

    /**
     * Renders a OK/Cancel/Validate/Expand All/Collapse All button bar.
     */
    @SuppressWarnings("serial")
    private Button renderButtons(VerticalLayout layout, boolean header) {
        CssLayout buttons = new CssLayout();
        buttons.addStyleName(STYLE_EDIT_PROJECT_BUTTONS);
        String prefix = header ? HEADER : FOOTER;

        Button okButton = new Button("OK");
        okButton.setIcon(ICON_BUTTON_OK);
        okButton.setDescription("Save the modified project");
        okButton.addStyleName(prefix + BUTTON_OK);
        okButton.addListener(new OKButtonListener());
        buttons.addComponent(okButton);

        Button cancelButton = new Button("Cancel");
        cancelButton.setIcon(ICON_BUTTON_CANCEL);
        cancelButton.setDescription("Discard all changes to the project");
        cancelButton.addStyleName(prefix + BUTTON_CANCEL);
        cancelButton.addListener(new CancelButtonListener());
        buttons.addComponent(cancelButton);

        Button checkButton = new Button("Check");
        checkButton.setIcon(ICON_BUTTON_VALIDATE);
        checkButton.setDescription("Checks the modified project for issues without saving it");
        checkButton.addStyleName(prefix + BUTTON_VALIDATE);
        checkButton.addListener(new Button.ClickListener() {
            @Override
            public void buttonClick(ClickEvent event) {
                validateModifiedProject();
            }
        });
        buttons.addComponent(checkButton);

        Button expandAllButton = new Button("Expand All");
        expandAllButton.setIcon(ICON_BUTTON_EXPAND_ALL);
        expandAllButton.setDescription("Expand all panels");
        expandAllButton.addStyleName(prefix + BUTTON_EXPAND_ALL);
        expandAllButton.addListener(new Button.ClickListener() {
            @Override
            public void buttonClick(ClickEvent event) {
                expandAllPanels();
            }
        });
        buttons.addComponent(expandAllButton);

        Button collapseAllButton = new Button("Collapse All");
        collapseAllButton.setIcon(ICON_BUTTON_COLLAPSE_ALL);
        collapseAllButton.setDescription("Collapse all panels");
        collapseAllButton.addStyleName(prefix + BUTTON_COLLAPSE_ALL);
        collapseAllButton.addListener(new Button.ClickListener() {
            @Override
            public void buttonClick(ClickEvent event) {
                collapseAllPanels();
            }
        });
        buttons.addComponent(collapseAllButton);

        layout.addComponent(buttons);
        layout.setComponentAlignment(buttons, Alignment.MIDDLE_CENTER);
        return checkButton;
    }

    /**
     * Renders the panels in the order defined by the comparator of {@link #sortedPanels}.
     */
    private void renderPanels(VerticalLayout layout) {
        for (ProjectEditPanelEntry entry : sortedPanels) {
            layout.addComponent(entry);
            layout.setComponentAlignment(entry, Alignment.MIDDLE_CENTER);
        }
    }

    private void collapseAllPanels() {
        for (ProjectEditPanelEntry entry : sortedPanels) {
            entry.collapse();
        }
    }

    private void expandAllPanels() {
        for (ProjectEditPanelEntry entry : sortedPanels) {
            entry.expand();
        }
    }

    /**
     * Validates the modified project with {@link Severity#INFO} and renders the result.
     */
    private void validateModifiedProject() {
        headerCheckButton.setEnabled(false);
        footerCheckButton.setEnabled(false);
        commitForms();
        renderIssues(null, false);
        progressIndicator.setValue(0f);
        indicatorArea.setVisible(true);
        progressThread = new ProgressThread();
        validatorThread = new ValidatorThread();
        progressThread.start();
        validatorThread.start();
    }

    private void updateProgressIndicator() {
        if (validatorThread != null && validatorThread.isFinished()) {
            indicatorArea.setVisible(false);
            if (persistedIssues != null) {
                persistedIssues.addLatestDuration(validatorThread.getDuration());
            }
            progressThread.interrupt();
            validatorThread.interrupt();
            progressThread = null;
            validatorThread = null;
            headerCheckButton.setEnabled(true);
            footerCheckButton.setEnabled(true);
        } else if (progressThread != null) {
            progressIndicator.setValue(progressThread.progress());
        } else {
            progressIndicator.setValue(0f);
        }
    }

    private class ProgressThread extends Thread {
        private static final long SLEEPING_TIME = 300L; // 1 seconds milliseconds
        private static final long UNKNOWN_AVERAGE_DURATION = 30000L; // 10 seconds

        private long elapsedTime;
        private long averageDuration;

        @Override
        public void run() {
            averageDuration = UNKNOWN_AVERAGE_DURATION;
            if (persistedIssues != null && persistedIssues.getAverageDuration() > 0) {
                averageDuration = persistedIssues.getAverageDuration();
            }

            for (; elapsedTime < averageDuration; elapsedTime += SLEEPING_TIME) {
                try {
                    Thread.sleep(SLEEPING_TIME);
                } catch (InterruptedException e) {
                    return;
                }
                synchronized (getApplication()) {
                    updateProgressIndicator();
                }
            }
        }

        private float progress() {
            return Math.min((float) elapsedTime / averageDuration, 0.95f); // never return 100%
        }
    }

    private class ValidatorThread extends Thread {
        private long startTime;
        private long duration = -1L;

        @Override
        public void run() {
            ProjectService projectService = ((ProjectService) EntityServices.getByEntityClass(Project.class));
            startTime = System.currentTimeMillis();
            SortedSet<Issue> issues = projectService.validate(modifiedProject, Severity.INFO);
            synchronized (getApplication()) {
                renderIssues(issues, false);
                if (issues.size() == 0) {
                    getWindow().showNotification("No Issues Found");
                }
                duration = System.currentTimeMillis() - startTime;
                updateProgressIndicator();
            }
        }

        public boolean isFinished() {
            return duration >= 0;
        }

        public long getDuration() {
            return duration;
        }
    }

    /**
     * Renders the persisted validation issues. If the persisted issues are
     * stale, render a corresponding message instead.
     */
    private void renderPersistedIssues() {
        if (persistedIssues != null) {
            if (persistedIssues.isStale()) {
                setMessage("<ul><li class=\"STALE\">"
                        + "No information about issues available. Use the <b>Validate</b> button "
                        + "above to validate the project now.</li></ul>");
            } else {
                renderIssues(persistedIssues.getIssues(), false);
            }
        }
    }

    /**
     * Renders validation issues given by a <code>ValidationException</code>
     * merged with the persisted issues (if any).
     *
     * @param e  the validation exception to render.
     */
    private void renderIssues(ValidationException e) {
        TreeSet<Issue> issues = new TreeSet<Issue>(e.getIssues());
        renderIssues(issues, true);
    }

    /**
     * Renders the given validation issues.
     *
     * If <code>collapseValid</code> is set all panels without issues will be collapsed
     * to focus the view of the user to panels with issues.
     */
    private void renderIssues(SortedSet<Issue> issues, boolean collapseValid) {
        Map<String, SortedSet<Issue>> sortedIssues = new HashMap<String, SortedSet<Issue>>();
        sortIssuesByExtension(issues, sortedIssues);

        // render issues that are related to an extension
        for (ProjectEditPanelEntry entry : sortedPanels) {
            String extensionName = entry.getExtensionClassName();
            SortedSet<Issue> extensionIssues = sortedIssues.get(extensionName);
            entry.showIssues(extensionIssues, collapseValid);
        }

        // render issues in the message areas
        setMessage(issues, displayNames);
    }

    /**
     * Sorts the issues by extension and adds them to <code>extensionIssues</code> according to the
     * extension they belong to.
     */
    private void sortIssuesByExtension(SortedSet<Issue> issues, Map<String, SortedSet<Issue>> extensionIssues) {
        if (issues != null) {
            for (Issue issue : issues) {
                Class<? extends ExtensionEntityBase> extension = issue.getExtension();
                if (extension != null) {
                    String extensionName = extension.getName();
                    addIssue(extensionIssues, extensionName, issue);
                }
            }
        }
    }

    private void addIssue(Map<String, SortedSet<Issue>> issues, String extensionName, Issue issue) {
        SortedSet<Issue> set = issues.get(extensionName);
        if (set == null) {
            set = new TreeSet<Issue>();
        }
        set.add(issue);
        issues.put(extensionName, set);
    }

    /**
     * Commits the forms of all enabled panels, so that fresh input from the Vaadin
     * fields is copied to the modified project. Note, this method does not persist
     * the modified project!
     */
    private void commitForms() {
        for (ProjectEditPanelEntry entry : sortedPanels) {
            if (entry.isEnabled()) {
                try {
                    entry.commit();
                } catch (InvalidValueException e) {
                    // we do not support Vaadin validation (see DefaultProjectFieldFactory),
                    // but if something bad happens, we at least should log it
                    LOG.error(e.getMessage(), e);
                }
            }
        }
    }

    /**
     * Persists the current modified project and removes persisted issues.
     *
     * Sets the current system time as {@link Project#setRegistered(long) registration time},
     * if a new project is saved for the first time.
     *
     * @throws ValidationException if there are {@link org.eclipse.skalli.model.ext.Severity#FATAL fatal}
     * validation issues.
     */
    private void commit() throws ValidationException {
        if (ProjectEditMode.NEW_PROJECT.equals(mode)) {
            modifiedProject.setRegistered(System.currentTimeMillis());
        }
        commitModifiedProject();
        clearPersistedIssues();
    }

    private void commitModifiedProject() throws ValidationException {
        ProjectService projectService = ((ProjectService) EntityServices.getByEntityClass(Project.class));
        projectService.persist(modifiedProject, application.getLoggedInUser());
    }

    /**
     * Removes persisted issues for this project, but marks the corresponding
     * {@link Issues} entry as stale.
     */
    private void clearPersistedIssues() throws ValidationException {
        IssuesService issuesService = Services.getService(IssuesService.class);
        if (issuesService != null) {
            Issues emptyIssues = new Issues(modifiedProject.getUuid());
            emptyIssues.setStale(true);
            issuesService.persist(emptyIssues, application.getLoggedInUser());
        }
    }

    private class OKButtonListener implements Button.ClickListener {
        private static final long serialVersionUID = 6531396291087032954L;

        @Override
        public void buttonClick(ClickEvent event) {
            try {
                commitForms();
                List<String> dataLossWarnings = getDataLossWarnings();
                List<String> confirmationWarnings = getConfirmationWarnings();
                if (dataLossWarnings.isEmpty() && confirmationWarnings.isEmpty()) {
                    doCommit();
                } else {
                    ConfirmPopup popup = new ConfirmPopup(dataLossWarnings, confirmationWarnings,
                            new ConfirmPopup.OnConfirmation() {
                                @Override
                                public void onConfirmation(boolean confirmed) {
                                    if (confirmed) {
                                        doCommit();
                                    }
                                }
                            });
                    getWindow().addWindow(popup);
                }
            } catch (RuntimeException e) {
                // If something bad happens in a validator, in the form commit or
                // while persisting the project, we log the incident and render the exception
                renderException(e);
                LOG.error(e.getMessage(), e);
            }
        }

        private void renderException(Throwable t) {
            StringBuilder sb = new StringBuilder();
            sb.append("<strong class=\"").append(STYLE_EDIT_PROJECT_ERROR).append("\">"); //$NON-NLS-1$ //$NON-NLS-2$
            sb.append("Failed to commit changes. An internal error occured. See log for details.");
            sb.append("</strong>"); //$NON-NLS-1$
            setMessage(sb.toString());
        }

        private void doCommit() {
            try {
                commit();
                application.refresh(modifiedProject);
                application.refresh(project.getParentProject());
                application.refresh(modifiedProject.getParentProject());
                if (!modifiedProject.isDeleted()) {
                    refreshSubProjects(modifiedProject);
                    navigator.navigateProjectView(modifiedProject);
                } else {
                    navigator.navigateWelcomeView();
                }
            } catch (ValidationException e) {
                renderIssues(e);
                LOG.debug(e.getMessage(), e);
            }
        }

        private void refreshSubProjects(EntityBase parent) {
            EntityBase next = parent.getFirstChild();
            while (next != null) {
                application.refresh((Project) next);
                refreshSubProjects(next);
                next = next.getNextSibling();
            }
        }

        private List<String> getDataLossWarnings() {
            List<String> warnings = new ArrayList<String>();
            for (ExtensionEntityBase extension : project.getAllExtensions()) {
                Class<? extends ExtensionEntityBase> extensionClass = extension.getClass();
                if (modifiedProject.isInherited(extensionClass) && !project.isInherited(extensionClass)) {
                    warnings.add(asWarning(WARN_EXTENSION_INHERITED, extension));
                } else if (modifiedProject.getExtension(extensionClass) == null) {
                    warnings.add(asWarning(WARN_EXTENSION_DISABLED, extension));
                }
            }
            return warnings;
        }

        private List<String> getConfirmationWarnings() {
            User modifier = UserServices.getUser(application.getLoggedInUser());
            List<String> warnings = new ArrayList<String>();
            for (ExtensionEntityBase extension : project.getAllExtensions()) {
                Class<? extends ExtensionEntityBase> extensionClass = extension.getClass();
                ExtensionService<?> extensionService = ExtensionServices.getByExtensionClass(extensionClass);
                if (extensionService != null) {
                    warnings.addAll(extensionService.getConfirmationWarnings(project, modifiedProject, modifier));
                }
            }
            return warnings;
        }

        private String asWarning(String pattern, ExtensionEntityBase extension) {
            String displayName = displayNames.get(extension.getClass().getName());
            if (pattern == WARN_EXTENSION_DISABLED) {
                return MessageFormat.format(pattern, displayName);
            }
            return MessageFormat.format(pattern, displayName,
                    ((Project) extension.getExtensibleEntity()).getName());
        }
    }

    private static class ConfirmPopup extends Window implements Button.ClickListener {
        private static final long serialVersionUID = 6695547216798144892L;

        public interface OnConfirmation {
            public void onConfirmation(boolean confirmed);
        }

        private OnConfirmation callback;
        private Button yes = new Button("Yes", this);
        private Button no = new Button("No", this);

        public ConfirmPopup(List<String> dataLossWarnings, List<String> confirmationWarnings,
                OnConfirmation callback) {
            super("Confirmation of Changes");
            setModal(true);
            setWidth(CONFIRM_POPUP_WIDTH);
            this.callback = callback;

            StringBuilder sb = new StringBuilder();
            append(sb, "The following changes will <strong>remove data permanently</strong> from the project:",
                    dataLossWarnings);
            append(sb, "The following changes might not be your intention:", confirmationWarnings);
            sb.append("<p>Continue anyway?</p>");
            Label content = new Label(sb.toString(), Label.CONTENT_XHTML);
            addComponent(content);

            HorizontalLayout hl = new HorizontalLayout();
            hl.setSpacing(true);
            hl.addComponent(yes);
            hl.addComponent(no);
            addComponent(hl);
        }

        @SuppressWarnings("nls")
        private void append(StringBuilder sb, String title, List<String> messages) {
            if (messages.size() > 0) {
                sb.append("<p>").append(title).append("</p>");
                sb.append("<p><ul>");
                for (String message : messages) {
                    sb.append("<li>").append(message).append("</li>");
                }
                sb.append("</ul></p>");
            }
        }

        @Override
        public void buttonClick(ClickEvent event) {
            if (getParent() != null) {
                ((Window) getParent()).removeWindow(this);
            }
            callback.onConfirmation(event.getSource() == yes);
        }
    }

    private class CancelButtonListener implements Button.ClickListener {
        private static final long serialVersionUID = 4567366927195161150L;

        @Override
        public void buttonClick(ClickEvent event) {
            if (mode.equals(ProjectEditMode.EDIT_PROJECT)) {
                navigator.navigateProjectView(project);
            } else if (mode.equals(ProjectEditMode.NEW_PROJECT)) {
                navigator.navigateWelcomeView();
            }
        }
    }

    private class Context implements ProjectEditContext {
        private final ProjectApplication application;
        private final ProjectTemplate projectTemplate;
        private final ProjectEditMode mode;

        public Context(ProjectApplication application, ProjectTemplate projectTemplate, ProjectEditMode mode) {
            this.projectTemplate = projectTemplate;
            this.application = application;
            this.mode = mode;
        }

        @Override
        public ProjectTemplate getProjectTemplate() {
            return projectTemplate;
        }

        @Override
        public boolean isAdministrator() {
            return GroupUtils.isAdministrator(application.getLoggedInUser());
        }

        @Override
        public ProjectEditMode getProjectEditMode() {
            return mode;
        }

        @Override
        public void onPropertyChanged(String extensionClassName, String propertyName, Object propertyValue) {
            for (ProjectEditPanelEntry entry : sortedPanels) {
                entry.onPropertyChanged(extensionClassName, propertyName, propertyValue);
            }
        }

        @Override
        public Object getProperty(String extensionClassName, String propertyName) {
            ProjectEditPanelEntry entry = panels.get(extensionClassName);
            return entry != null ? entry.getProperty(propertyName) : null;
        }

        @Override
        public void setProperty(String extensionClassName, String propertyName, Object propertyValue) {
            ProjectEditPanelEntry entry = panels.get(extensionClassName);
            if (entry != null) {
                entry.setProperty(propertyName, propertyValue);
            }
        }

        @Override
        public boolean hasPanel(String extensionClassName) {
            return panels.get(extensionClassName) != null;
        }

        @Override
        public boolean isEditable(String extensionClassName) {
            ProjectEditPanelEntry entry = panels.get(extensionClassName);
            if (entry == null) {
                throw new IllegalArgumentException(
                        MessageFormat.format("No panel for extension {0}", extensionClassName));
            }
            return entry.isEditable();
        }

        @Override
        public boolean isInherited(String extensionClassName) {
            ProjectEditPanelEntry entry = panels.get(extensionClassName);
            if (entry == null) {
                throw new IllegalArgumentException(
                        MessageFormat.format("No panel for extension {0}", extensionClassName));
            }
            return entry.isInherited();
        }

        @Override
        public boolean isDisabled(String extensionClassName) {
            ProjectEditPanelEntry entry = panels.get(extensionClassName);
            if (entry == null) {
                throw new IllegalArgumentException(
                        MessageFormat.format("No panel for extension {0}", extensionClassName));
            }
            return entry.isDisabled();
        }

        @Override
        public boolean isExpanded(String extensionClassName) {
            ProjectEditPanelEntry entry = panels.get(extensionClassName);
            if (entry == null) {
                throw new IllegalArgumentException(
                        MessageFormat.format("No panel for extension {0}", extensionClassName));
            }
            return entry.isExpanded();
        }
    }

    private static class ProjectEditPanelComparator implements Comparator<ProjectEditPanelEntry> {
        private ProjectTemplate projectTemplate;

        public ProjectEditPanelComparator(ProjectTemplate projectTemplate) {
            this.projectTemplate = projectTemplate;
        }

        @Override
        public int compare(ProjectEditPanelEntry o1, ProjectEditPanelEntry o2) {
            int result = Float.compare(getRank(o1), getRank(o2));
            if (result == 0) {
                // same rank: compare states (required < visible < enabled
                result = compareStates(o1, o2);
                if (result == 0) {
                    // same rank and state: order captions alphabetically
                    result = o1.getDisplayName().compareTo(o2.getDisplayName());
                }
            }
            return result;
        }

        private float getRank(ProjectEditPanelEntry o) {
            float rank = projectTemplate.getRank(o.getExtensionClassName());
            if (rank < 0) {
                rank = o.getRank();
                if (rank < 0) {
                    rank = Float.MAX_VALUE;
                }
            }
            return rank;
        }

        private int compareStates(ProjectEditPanelEntry o1, ProjectEditPanelEntry o2) {
            String ext1 = o1.getExtensionClassName();
            String ext2 = o2.getExtensionClassName();
            int result = compareStates(projectTemplate.isVisible(ext1), projectTemplate.isVisible(ext2));
            if (result == 0) {
                result = compareStates(projectTemplate.isEnabled(ext1), projectTemplate.isEnabled(ext2));
            }
            return result;
        }

        private int compareStates(boolean state1, boolean state2) {
            if (state1) {
                return state2 ? 0 : -1;
            } else {
                return state2 ? 1 : 0;
            }
        }
    }
}