org.zanata.action.ProjectHome.java Source code

Java tutorial

Introduction

Here is the source code for org.zanata.action.ProjectHome.java

Source

/*
 * Copyright 2010, Red Hat, Inc. and individual contributors as indicated by the
 * @author tags. See the copyright.txt file in the distribution for a full
 * listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This software is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this software; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
 * site: http://www.fsf.org.
 */
package org.zanata.action;

import static com.google.common.base.Strings.isNullOrEmpty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Model;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ViewScoped;
import javax.faces.event.ValueChangeEvent;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.Session;
import org.hibernate.criterion.NaturalIdentifier;
import org.hibernate.criterion.Restrictions;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.constraints.NotNull;

import org.apache.deltaspike.jpa.api.transaction.Transactional;
import org.zanata.common.EntityStatus;
import org.zanata.common.LocaleId;
import org.zanata.common.ProjectType;
import org.zanata.dao.AccountRoleDAO;
import org.zanata.dao.LocaleDAO;
import org.zanata.dao.PersonDAO;
import org.zanata.dao.WebHookDAO;
import org.zanata.exception.ProjectNotFoundException;
import org.zanata.i18n.Messages;
import org.zanata.model.HAccount;
import org.zanata.model.HAccountRole;
import org.zanata.model.HLocale;
import org.zanata.model.HPerson;
import org.zanata.model.HProject;
import org.zanata.model.HProjectIteration;
import org.zanata.model.WebHook;
import org.zanata.model.type.WebhookType;
import org.zanata.model.validator.SlugValidator;
import org.zanata.security.ZanataIdentity;
import org.zanata.security.annotations.Authenticated;
import org.zanata.service.LocaleService;
import org.zanata.service.ProjectService;
import org.zanata.service.SlugEntityService;
import org.zanata.service.ValidationService;
import org.zanata.service.impl.WebhookServiceImpl;
import org.zanata.ui.AbstractListFilter;
import org.zanata.ui.InMemoryListFilter;
import org.zanata.ui.autocomplete.MaintainerAutocomplete;
import org.zanata.ui.faces.FacesMessages;
import org.zanata.util.CommonMarkRenderer;
import org.zanata.util.ComparatorUtil;
import org.zanata.util.UrlUtil;
import org.zanata.webhook.events.ProjectMaintainerChangedEvent;
import org.zanata.webtrans.shared.model.ValidationAction;
import org.zanata.webtrans.shared.model.ValidationId;
import org.zanata.webtrans.shared.validation.ValidationFactory;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import static javax.faces.application.FacesMessage.SEVERITY_ERROR;
import static javax.faces.application.FacesMessage.SEVERITY_INFO;
import static org.zanata.service.impl.WebhookServiceImpl.getTypesFromString;
import static org.zanata.model.ProjectRole.Maintainer;

@Named("projectHome")
@ViewScoped
@Model
@Transactional
public class ProjectHome extends SlugHome<HProject> implements HasLanguageSettings {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ProjectHome.class);
    private static final long serialVersionUID = 1L;

    // /**
    // * This field is set from http parameter which will be the original slug
    // */
    // @Getter
    // private String slug;
    @Inject
    @Any
    private ProjectSlug projectSlug;

    /**
     * This field is set from form input which can differ from original slug
     */
    @Nullable
    private String inputSlugValue;
    private Long projectId;
    @Inject
    private ZanataIdentity identity;
    @Inject
    @Authenticated
    private HAccount authenticatedAccount;
    @Inject
    private LocaleService localeServiceImpl;
    @Inject
    private LocaleDAO localeDAO;
    @Inject
    private SlugEntityService slugEntityServiceImpl;
    @Inject
    private CommonMarkRenderer renderer;
    @SuppressFBWarnings("SE_BAD_FIELD")
    @Inject
    private EntityManager entityManager;
    @Inject
    private FacesMessages facesMessages;
    @Inject
    private Messages msgs;
    @Inject
    private PersonDAO personDAO;
    @Inject
    private AccountRoleDAO accountRoleDAO;
    @Inject
    private WebHookDAO webHookDAO;
    @Inject
    private ValidationService validationServiceImpl;
    @Inject
    private CopyTransOptionsModel copyTransOptionsModel;
    @Inject
    private WebhookServiceImpl webhookServiceImpl;
    @Inject
    private ProjectService projectServiceImpl;
    @Inject
    private UrlUtil urlUtil;
    // This property is present to keep the filter in place when the region with
    // the filter box is refreshed.
    private String enabledLocalesFilter = "";
    private String disabledLocalesFilter;

    /**
     * A separate map is used, rather than binding the alias map from the
     * project directly. This is done so that empty values are not added to the
     * map in every form submission, and so that a value entered in the field
     * for a row is not automatically updated when a different row is submitted.
     */
    private Map<LocaleId, String> enteredLocaleAliases = Maps.newHashMap();
    private Map<LocaleId, Boolean> selectedEnabledLocales = Maps.newHashMap();
    // Not sure if this is necessary, seems to work ok on selected disabled
    // locales without this.

    public Map<LocaleId, Boolean> getSelectedEnabledLocales() {
        if (selectedEnabledLocales == null) {
            selectedEnabledLocales = Maps.newHashMap();
            for (HLocale locale : getEnabledLocales()) {
                selectedEnabledLocales.put(locale.getLocaleId(), Boolean.FALSE);
            }
        }
        return selectedEnabledLocales;
    }

    private Map<LocaleId, Boolean> selectedDisabledLocales = Maps.newHashMap();
    private Boolean selectedCheckbox = Boolean.TRUE;
    private List<HLocale> disabledLocales;

    public ProjectHome() {
        setEntityClass(HProject.class);
    }

    public String getSlug() {
        return projectSlug.getValue();
    }

    public List<HLocale> getDisabledLocales() {
        if (disabledLocales == null) {
            disabledLocales = findActiveNotEnabledLocales();
        }
        return disabledLocales;
    }

    /**
     * Populate the list of available locales after filtering out the locales
     * already in the project.
     */
    private List<HLocale> findActiveNotEnabledLocales() {
        List<HLocale> activeLocales = localeDAO.findAllActive();
        // only include those not already in the project
        List<HLocale> filteredList = activeLocales.stream()
                .filter(hLocale -> !getEnabledLocales().contains(hLocale)).collect(Collectors.toList());
        Collections.sort(filteredList, ComparatorUtil.LOCALE_COMPARATOR);
        return filteredList;
    }

    private Map<String, Boolean> roleRestrictions;
    @SuppressFBWarnings("SE_BAD_FIELD")
    private Map<ValidationId, ValidationAction> availableValidations = Maps.newHashMap();
    private final java.util.concurrent.atomic.AtomicReference<List<HProjectIteration>> versions = new java.util.concurrent.atomic.AtomicReference<>();
    private String selectedProjectType;
    @Inject
    private ProjectMaintainersAutocomplete maintainerAutocomplete;
    private AbstractListFilter<HPerson> maintainerFilter = new InMemoryListFilter<HPerson>() {

        private static final long serialVersionUID = 8259700829800303578L;

        @Override
        protected List<HPerson> fetchAll() {
            return getInstanceMaintainers();
        }

        @Override
        protected boolean include(HPerson elem, String filter) {
            return StringUtils.containsIgnoreCase(elem.getName(), filter);
        }
    };

    public void createNew() {
        clearSlugs();
        HProject instance = getInstance();
        identity.checkPermission(instance, "insert");
        instance.setDefaultProjectType(ProjectType.File);
        selectedProjectType = ProjectType.File.name();
        enteredLocaleAliases.putAll(getLocaleAliases());
        // force get so it will create and populate the hashmap
        getSelectedEnabledLocales();
    }

    @Transactional
    public void updateSelectedProjectType(ValueChangeEvent e) {
        selectedProjectType = (String) e.getNewValue();
        updateProjectType();
    }

    @Transactional
    public void setSelectedProjectType(String selectedProjectType) {
        if (!StringUtils.isEmpty(selectedProjectType) && !selectedProjectType.equals("null")) {
            ProjectType projectType = ProjectType.valueOf(selectedProjectType);
            getInstance().setDefaultProjectType(projectType);
        }
    }

    public boolean isOverrideLocales() {
        return getInstance().isOverrideLocales();
    }

    @Transactional
    public void setOverrideLocales(boolean overrideLocales) {
        getInstance().setOverrideLocales(overrideLocales);
    }

    /**
     * Return the list of enabled locales for this project, which may be
     * inherited from global locales. If the project slug is empty, all the
     * enabled locales for the server are returned.
     */
    public List<HLocale> getEnabledLocales() {
        List<HLocale> locales;
        if (StringUtils.isNotEmpty(getSlug())) {
            locales = localeServiceImpl.getSupportedLanguageByProject(getSlug());
        } else {
            locales = localeServiceImpl.getSupportedAndEnabledLocales();
        }
        Collections.sort(locales, ComparatorUtil.LOCALE_COMPARATOR);
        return locales;
    }

    public Map<LocaleId, String> getLocaleAliases() {
        return getInstance().getLocaleAliases();
    }

    private void setLocaleAliases(Map<LocaleId, String> localeAliases) {
        getInstance().setLocaleAliases(localeAliases);
    }

    /**
     * Return the locale alias for the given locale in this project, if it
     * exists, otherwise null.
     */
    public String getLocaleAlias(HLocale locale) {
        return getLocaleAliases().get(locale.getLocaleId());
    }

    /**
     * Return true if the given locale has an alias, otherwise false.
     */
    public boolean hasLocaleAlias(HLocale locale) {
        return getLocaleAliases().containsKey(locale.getLocaleId());
    }

    /**
     * Set or remove a locale alias based on form input.
     *
     * Uses value from enteredLocaleAlias. If the value is null or empty, the
     * alias (if any) is removed for the given locale, otherwise the alias is
     * replaced with the value.
     */
    @Transactional
    public void updateToEnteredLocaleAlias(LocaleId localeId) {
        identity.checkPermission(getInstance(), "update");
        String enteredAlias = enteredLocaleAliases.get(localeId);
        setLocaleAlias(localeId, enteredAlias);
    }

    private void setLocaleAlias(LocaleId localeId, String alias) {
        boolean hadAlias = setLocaleAliasSilently(localeId, alias);
        if (isNullOrEmpty(alias)) {
            if (hadAlias) {
                facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                        msgs.format("jsf.LocaleAlias.AliasRemoved", localeId));
            } else {
                facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                        msgs.format("jsf.LocaleAlias.NoAliasToRemove", localeId));
            }
        } else {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                    msgs.format("jsf.LocaleAlias.AliasSet", localeId, alias));
        }
    }

    /**
     * Set or remove a locale alias without showing any message.
     *
     * @param localeId
     *            for which to set alias
     * @param alias
     *            new alias to use. Use empty string to remove alias.
     * @return true if there was already an alias, otherwise false.
     */
    private boolean setLocaleAliasSilently(LocaleId localeId, String alias) {
        HProject instance = getInstance();
        Map<LocaleId, String> aliases = instance.getLocaleAliases();
        boolean hadAlias = aliases.containsKey(localeId);
        if (isNullOrEmpty(alias)) {
            if (hadAlias) {
                // no need to ensure overriding locales, aliases are independent
                aliases.remove(localeId);
            }
        } else {
            final boolean sameAlias = hadAlias && alias.equals(aliases.get(localeId));
            if (!sameAlias) {
                // no need to ensure overriding locales, aliases are independent
                aliases.put(localeId, alias);
            }
        }
        update();
        return hadAlias;
    }

    /**
     * Remove a locale alias without showing any message.
     *
     * @param localeId
     *            that will have its locale alias removed.
     * @return true if the locale had an alias, otherwise false.
     */
    private boolean removeAliasSilently(LocaleId localeId) {
        return setLocaleAliasSilently(localeId, "");
    }

    @Transactional
    public void removeSelectedLocaleAliases() {
        identity.checkPermission(getInstance(), "update");
        List<LocaleId> removed = new ArrayList<>();
        for (Map.Entry<LocaleId, Boolean> entry : getSelectedEnabledLocales().entrySet()) {
            if (entry.getValue()) {
                boolean hadAlias = removeAliasSilently(entry.getKey());
                if (hadAlias) {
                    removed.add(entry.getKey());
                }
            }
        }
        showRemovedAliasesMessage(removed);
    }

    @Transactional
    public void removeAllLocaleAliases() {
        identity.checkPermission(getInstance(), "update");
        List<LocaleId> removed = new ArrayList<>();
        List<LocaleId> aliasedLocales = new ArrayList<>(getLocaleAliases().keySet());
        for (LocaleId aliasedLocale : aliasedLocales) {
            boolean hadAlias = removeAliasSilently(aliasedLocale);
            if (hadAlias) {
                removed.add(aliasedLocale);
            }
        }
        showRemovedAliasesMessage(removed);
    }

    /**
     * Show an appropriate message for the removal of aliases from locales with
     * the given IDs.
     *
     * @param removed
     *            ids of locales that had aliases removed
     */
    private void showRemovedAliasesMessage(List<LocaleId> removed) {
        if (removed.isEmpty()) {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, msgs.get("jsf.LocaleAlias.NoAliasesToRemove"));
        } else if (removed.size() == 1) {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                    msgs.format("jsf.LocaleAlias.AliasRemoved", removed.get(0)));
        } else {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                    msgs.format("jsf.LocaleAlias.AliasesRemoved", StringUtils.join(removed, ", ")));
        }
    }

    @Transactional
    public void disableLocale(HLocale locale) {
        identity.checkPermission(getInstance(), "update");
        disableLocaleSilently(locale);
        facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                msgs.format("jsf.languageSettings.LanguageDisabled", locale.getLocaleId()));
    }

    @Transactional
    public void disableSelectedLocales() {
        identity.checkPermission(getInstance(), "update");
        List<LocaleId> removedLocales = new ArrayList<>();
        for (Map.Entry<LocaleId, Boolean> entry : getSelectedEnabledLocales().entrySet()) {
            if (entry.getValue()) {
                boolean wasEnabled = disableLocaleSilently(entry.getKey());
                if (wasEnabled) {
                    removedLocales.add(entry.getKey());
                }
            }
        }
        selectedEnabledLocales.clear();
        if (removedLocales.isEmpty()) {
            // This should not be possible in the UI, but maybe if multiple
            // users are editing it.
        } else if (removedLocales.size() == 1) {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                    msgs.format("jsf.languageSettings.LanguageDisabled", removedLocales.get(0)));
        } else {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                    msgs.format("jsf.languageSettings.LanguagesDisabled", StringUtils.join(removedLocales, ", ")));
        }
    }

    private boolean disableLocaleSilently(LocaleId localeId) {
        HLocale locale = localeServiceImpl.getByLocaleId(localeId);
        return disableLocaleSilently(locale);
    }

    /**
     * Disable a locale without printing any message.
     *
     * @param locale
     *            locale that should be disabled.
     * @return false if the locale was already disabled, true otherwise.
     */
    private boolean disableLocaleSilently(HLocale locale) {
        final Set<HLocale> customizedLocales = getInstance().getCustomizedLocales();
        ensureOverridingLocales();
        boolean localeWasEnabled = customizedLocales.remove(locale);
        getLocaleAliases().remove(locale.getLocaleId());
        refreshDisabledLocales();
        return localeWasEnabled;
    }

    @Transactional
    public void enableLocale(HLocale locale) {
        identity.checkPermission(getInstance(), "update");
        enableLocaleSilently(locale);
        LocaleId localeId = locale.getLocaleId();
        facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                msgs.format("jsf.languageSettings.LanguageEnabled", localeId));
    }

    @Transactional
    public void enableSelectedLocales() {
        identity.checkPermission(getInstance(), "update");
        List<LocaleId> addedLocales = new ArrayList<>();
        for (Map.Entry<LocaleId, Boolean> entry : selectedDisabledLocales.entrySet()) {
            if (entry.getValue()) {
                boolean wasDisabled = enableLocaleSilently(entry.getKey());
                if (wasDisabled) {
                    addedLocales.add(entry.getKey());
                }
            }
        }
        selectedDisabledLocales.clear();
        if (addedLocales.isEmpty()) {
            // This should not be possible in the UI, but maybe if multiple
            // users are editing it.
        } else if (addedLocales.size() == 1) {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                    msgs.format("jsf.languageSettings.LanguageEnabled", addedLocales.get(0)));
        } else {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                    msgs.format("jsf.languageSettings.LanguagesEnabled", StringUtils.join(addedLocales, ", ")));
        }
    }

    private boolean enableLocaleSilently(LocaleId localeId) {
        HLocale locale = localeServiceImpl.getByLocaleId(localeId);
        return enableLocaleSilently(locale);
    }

    /**
     * Enable a given locale without printing any message.
     *
     * @param locale
     *            locale that should be enabled.
     * @return false if the locale was already enabled, true otherwise.
     */
    private boolean enableLocaleSilently(HLocale locale) {
        ensureOverridingLocales();
        final boolean localeWasDisabled = getInstance().getCustomizedLocales().add(locale);
        refreshDisabledLocales();
        return localeWasDisabled;
    }

    @Transactional
    public void useDefaultLocales() {
        identity.checkPermission(getInstance(), "update");
        setOverrideLocales(false);
        removeAliasesForDisabledLocales();
        refreshDisabledLocales();
        update();
        facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, msgs.get("jsf.project.LanguageUpdateFromGlobal"));
    }

    private void removeAliasesForDisabledLocales() {
        Map<LocaleId, String> oldAliases = getLocaleAliases();
        Map<LocaleId, String> newAliases = Maps.newHashMap();
        for (HLocale enabledLocale : getEnabledLocales()) {
            LocaleId key = enabledLocale.getLocaleId();
            if (oldAliases.containsKey(key)) {
                newAliases.put(key, oldAliases.get(key));
            }
        }
        setLocaleAliases(newAliases);
    }

    /**
     * Ensure that isOverrideLocales is true, and copy data if necessary.
     */
    private void ensureOverridingLocales() {
        if (!isOverrideLocales()) {
            startOverridingLocales();
        }
    }

    /**
     * Copy locale data from project and set overrideLocales, in preparation for
     * making customizations to the locales.
     */
    private void startOverridingLocales() {
        // Copied before setOverrideLocales(true) so that the currently returned
        // values will be used as the basis for any customization.
        List<HLocale> enabledLocales = getEnabledLocales();
        setOverrideLocales(true);
        // Replace contents rather than entire collections to avoid confusion
        // with reference to the collections that are bound before this runs.
        getInstance().getCustomizedLocales().clear();
        getInstance().getCustomizedLocales().addAll(enabledLocales);
        enteredLocaleAliases.clear();
        refreshDisabledLocales();
    }

    /**
     * Update disabled locales to be consistent with enabled locales.
     */
    private void refreshDisabledLocales() {
        // will be re-generated with correct values next time it is fetched.
        disabledLocales = null;
    }

    @Transactional
    public void setRestrictedByRole(String key, boolean checked) {
        identity.checkPermission(getInstance(), "update");
        getInstance().setRestrictedByRoles(checked);
        update();
    }

    @Transactional
    public void setPrivateProject(boolean privateProject) {
        identity.checkPermission(getInstance(), "update");
        getInstance().setPrivateProject(privateProject);
        update();
        String message = privateProject ? msgs.get("jsf.permission.private.Active")
                : msgs.get("jsf.permission.private.Inactive");
        facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, message);
    }

    @Override
    protected HProject loadInstance() {
        Session session = (Session) getEntityManager().getDelegate();
        if (projectId == null) {
            HProject project = (HProject) session.byNaturalId(HProject.class).using("slug", getSlug()).load();
            validateProjectState(project);
            projectId = project.getId();
            return project;
        } else {
            HProject project = (HProject) session.byId(HProject.class).load(projectId);
            validateProjectState(project);
            return project;
        }
    }

    private void validateProjectState(HProject project) {
        if (project == null || project.getStatus() == EntityStatus.OBSOLETE) {
            log.warn("Project [id={}, slug={}], does not exist or is soft deleted: {}", projectId, getSlug(),
                    project);
            throw new ProjectNotFoundException(getSlug());
        }
    }

    public void validateSuppliedId() {
        HProject ip = getInstance(); // this will raise an EntityNotFound
        // exception
        // when id is invalid and conversation will not
        // start
        if (ip.getStatus().equals(EntityStatus.OBSOLETE)) {
            throw new EntityNotFoundException();
        }
    }

    @Transactional
    public void updateCopyTrans(String action, String value) {
        copyTransOptionsModel.setInstance(getInstance().getDefaultCopyTransOpts());
        copyTransOptionsModel.update(action, value);
        copyTransOptionsModel.save();
        getInstance().setDefaultCopyTransOpts(copyTransOptionsModel.getInstance());
        update();
        facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, msgs.get("jsf.project.CopyTransOpts.updated"));
    }
    // @Begin(join = true) /* TODO [CDI] commented out begin conversation.
    // Verify it still works properly */

    public void initialize() {
        validateSuppliedId();
        if (getInstance().getDefaultCopyTransOpts() != null) {
            copyTransOptionsModel.setInstance(getInstance().getDefaultCopyTransOpts());
        }
    }

    public void onProjectNameChange(ValueChangeEvent e) {
        if (!isValidName((String) e.getNewValue())) {
            String componentId = e.getComponent().getId();
            facesMessages.addToControl(componentId, msgs.get("jsf.project.name.validation.alphanumeric"));
        }
    }

    /**
     * Check the name by removing any whitespaces in the string and
     * make sure it contains at least an alphanumeric char
     */
    public boolean isValidName(String name) {
        String trimmedName = StringUtils.deleteWhitespace(name);
        for (char c : trimmedName.toCharArray()) {
            if (Character.isDigit(c) || Character.isLetter(c)) {
                return true;
            }
        }
        return false;
    }

    public void verifySlugAvailable(ValueChangeEvent e) {
        String slug = (String) e.getNewValue();
        validateSlug(slug, e.getComponent().getId());
    }

    public boolean validateSlug(String slug, String componentId) {
        if (!isSlugAvailable(slug)) {
            facesMessages.addToControl(componentId, "This Project ID is not available");
            return false;
        }
        boolean valid = new SlugValidator().isValid(slug, null);
        if (!valid) {
            String validationMessages = ResourceBundle.getBundle("ValidationMessages")
                    .getString("javax.validation.constraints.Slug.message");
            facesMessages.addToControl(componentId, validationMessages);
            return false;
        }
        return true;
    }

    public boolean isSlugAvailable(String slug) {
        return slugEntityServiceImpl.isSlugAvailable(slug, HProject.class);
    }

    private void updateProjectType() {
        if (!StringUtils.isEmpty(selectedProjectType) && !selectedProjectType.equals("null")) {
            ProjectType projectType = ProjectType.valueOf(selectedProjectType);
            getInstance().setDefaultProjectType(projectType);
        }
    }

    public void setSlug(String slug) {
        this.projectSlug.setValue(slug);
        this.inputSlugValue = slug;
    }

    @Override
    @Transactional
    public String update() {
        identity.checkPermission(getInstance(), "update");
        // getInputSlugValue() can be null
        if (!getSlug().equals(getInputSlugValue()) && !validateSlug(getInputSlugValue(), "slug")) {
            return null;
        }
        if (getInputSlugValue() != null && !getSlug().equals(getInputSlugValue())) {
            getInstance().setSlug(getInputSlugValue());
        }
        if (!isValidName(getInstance().getName())) {
            facesMessages.addGlobal(SEVERITY_ERROR, msgs.get("jsf.project.name.validation.alphanumeric"));
            return null;
        }
        boolean softDeleted = false;
        if (getInstance().getStatus() == EntityStatus.OBSOLETE) {
            softDeleted = true;
            getInstance().setSlug(getInstance().changeToDeletedSlug());
        }
        String result = super.update();
        if (softDeleted) {
            String url = urlUtil.dashboardUrl();
            urlUtil.redirectToInternal(url);
            return result;
        }
        facesMessages.addGlobal(SEVERITY_INFO, msgs.get("jsf.project.settings.updated"));
        if (!getSlug().equals(getInstance().getSlug())) {
            projectSlug.setValue(getInstance().getSlug());
            return "project-slug-updated";
        }
        return result;
    }

    @Override
    @Transactional
    public String persist() {
        String retValue = "";
        if (!validateSlug(getInputSlugValue(), "slug")) {
            return null;
        }
        getInstance().setSlug(getInputSlugValue());
        if (!isValidName(getInstance().getName())) {
            facesMessages.addGlobal(SEVERITY_ERROR, msgs.get("jsf.project.name.validation.alphanumeric"));
            return null;
        }
        if (StringUtils.isEmpty(selectedProjectType) || selectedProjectType.equals("null")) {
            facesMessages.addGlobal(SEVERITY_ERROR, "Project type not selected");
            return null;
        }
        if (StringUtils.isEmpty(selectedProjectType) || selectedProjectType.equals("null")) {
            facesMessages.addGlobal(SEVERITY_ERROR, "Project type not selected");
            return null;
        }
        updateProjectType();
        if (authenticatedAccount != null) {
            // authenticatedAccount person is a detached entity, so fetch a copy
            // that is attached to the current session.
            HPerson creator = personDAO.findById(authenticatedAccount.getPerson().getId());
            getInstance().addMaintainer(creator);
            getInstance().getCustomizedValidations().clear();
            for (ValidationAction validationAction : validationServiceImpl.getValidationActions("")) {
                getInstance().getCustomizedValidations().put(validationAction.getId().name(),
                        validationAction.getState().name());
            }
            retValue = super.persist();
            webhookServiceImpl.processWebhookMaintainerChanged(getInstance().getSlug(),
                    creator.getAccount().getUsername(), Maintainer, getInstance().getWebHooks(),
                    ProjectMaintainerChangedEvent.ChangeType.ADD);
        }
        return retValue;
    }

    /**
     * Returns the rendered, sanitised HTML for the about page content set by
     * the project maintainer.
     *
     * @return
     */
    public String getAboutHtml() {
        // we could cache this, but it may not be worth it
        String text = getInstance().getHomeContent();
        return renderer.renderToHtmlSafe(text);
    }

    public List<HPerson> getInstanceMaintainers() {
        List<HPerson> list = Lists.newArrayList(getInstance().getMaintainers());
        Collections.sort(list, ComparatorUtil.PERSON_NAME_COMPARATOR);
        return list;
    }

    @Transactional
    public String removeMaintainer(HPerson person) {
        identity.checkPermission(getInstance(), "update");
        if (getInstanceMaintainers().size() <= 1) {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, msgs.get("jsf.project.NeedAtLeastOneMaintainer"));
        } else {
            getInstance().removeMaintainer(person);
            maintainerFilter.reset();
            update();
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                    msgs.format("jsf.project.MaintainerRemoved", person.getName()));
            webhookServiceImpl.processWebhookMaintainerChanged(getSlug(), person.getAccount().getUsername(),
                    Maintainer, getInstance().getWebHooks(), ProjectMaintainerChangedEvent.ChangeType.REMOVE);
            if (person.equals(authenticatedAccount.getPerson())) {
                urlUtil.redirectToInternal(urlUtil.projectUrl(getSlug()));
            }
        }
        return "";
    }

    @Transactional
    public void updateRoles(String roleName, boolean isRestricted) {
        identity.checkPermission(getInstance(), "update");
        getInstance().getAllowedRoles().clear();
        if (getInstance().isRestrictedByRoles()) {
            getRoleRestrictions().put(roleName, isRestricted);
            for (Map.Entry<String, Boolean> entry : getRoleRestrictions().entrySet()) {
                if (entry.getValue()) {
                    getInstance().getAllowedRoles().add(accountRoleDAO.findByName(entry.getKey()));
                }
            }
        }
        update();
        facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, msgs.get("jsf.RolesUpdated"));
    }

    @Transactional
    public void updateStatus(char initial) {
        identity.checkPermission(getInstance(), "update");
        getInstance().setStatus(EntityStatus.valueOf(initial));
        if (getInstance().getStatus() == EntityStatus.READONLY) {
            for (HProjectIteration version : getInstance().getProjectIterations()) {
                if (version.getStatus() == EntityStatus.ACTIVE) {
                    version.setStatus(EntityStatus.READONLY);
                    entityManager.merge(version);
                }
            }
        } else if (getInstance().getStatus() == EntityStatus.OBSOLETE) {
            for (HProjectIteration version : getInstance().getProjectIterations()) {
                if (version.getStatus() != EntityStatus.OBSOLETE) {
                    version.setStatus(EntityStatus.OBSOLETE);
                    entityManager.merge(version);
                }
            }
        }
        update();
        EntityStatus status = EntityStatus.valueOf(initial);
        if (status.equals(EntityStatus.OBSOLETE)) {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                    msgs.format("jsf.project.notification.deleted", getSlug()));
        } else {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, msgs.format("jsf.project.status.updated", status));
        }
    }

    @Transactional
    public void deleteSelf() {
        updateStatus('O');
    }

    public Map<String, Boolean> getRoleRestrictions() {
        if (roleRestrictions == null) {
            roleRestrictions = Maps.newHashMap();
            for (HAccountRole role : getInstance().getAllowedRoles()) {
                roleRestrictions.put(role.getName(), true);
            }
        }
        return roleRestrictions;
    }

    public boolean isRoleRestrictionEnabled(String roleName) {
        if (getRoleRestrictions().containsKey(roleName)) {
            return getRoleRestrictions().get(roleName);
        }
        return false;
    }

    public List<HAccountRole> getAvailableRoles() {
        List<HAccountRole> allRoles = accountRoleDAO.findAll();
        allRoles.sort(ComparatorUtil.ACCOUNT_ROLE_COMPARATOR);
        return allRoles;
    }

    private @NotNull List<HProjectIteration> fetchVersions() {
        return getInstance().getProjectIterations().stream().filter(it -> it.getStatus() != EntityStatus.OBSOLETE)
                .sorted((o1, o2) -> {
                    EntityStatus fromStatus = o1.getStatus();
                    EntityStatus toStatus = o2.getStatus();
                    if (fromStatus.equals(toStatus)) {
                        return 0;
                    }
                    if (fromStatus.equals(EntityStatus.ACTIVE)) {
                        return -1;
                    }
                    if (fromStatus.equals(EntityStatus.READONLY)) {
                        if (toStatus.equals(EntityStatus.ACTIVE)) {
                            return 1;
                        }
                        return -1;
                    }
                    return 0;
                }).collect(Collectors.toList());
    }

    @Override
    public boolean isIdDefined() {
        return getSlug() != null;
    }

    @Override
    public NaturalIdentifier getNaturalId() {
        return Restrictions.naturalId().set("slug", getSlug());
    }

    @Override
    public Object getId() {
        return getSlug();
    }

    private Map<ValidationId, ValidationAction> getValidations() {
        if (availableValidations.isEmpty()) {
            Collection<ValidationAction> validationList = validationServiceImpl
                    .getValidationActions(getInstance().getSlug());
            for (ValidationAction validationAction : validationList) {
                availableValidations.put(validationAction.getId(), validationAction);
            }
        }
        return availableValidations;
    }

    @Transactional
    public void updateValidationOption(String name, String state) {
        identity.checkPermission(getInstance(), "update");
        ValidationId validationId = ValidationId.valueOf(name);
        for (Map.Entry<ValidationId, ValidationAction> entry : getValidations().entrySet()) {
            if (entry.getKey().name().equals(name)) {
                getValidations().get(validationId).setState(ValidationAction.State.valueOf(state));
                getInstance().getCustomizedValidations().put(entry.getKey().name(),
                        entry.getValue().getState().name());
                ensureMutualExclusivity(getValidations().get(validationId));
                break;
            }
        }
        update();
        facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                msgs.format("jsf.validation.updated", validationId.getDisplayName(), state));
    }

    public List<ValidationAction> getValidationList() {
        List<ValidationAction> sortedList = Lists.newArrayList(getValidations().values());
        Collections.sort(sortedList, ValidationFactory.ValidationActionComparator);
        return sortedList;
    }

    @Transactional
    public void addWebHook(String url, String secret, String strTypes, String name) {
        identity.checkPermission(getInstance(), "update");
        Set<WebhookType> types = getTypesFromString(strTypes);
        if (types.isEmpty()) {
            facesMessages.addGlobal(msgs.get("jsf.project.webhookType.empty"));
            return;
        }
        if (!isValidUrl(url)) {
            return;
        }
        if (projectServiceImpl.isDuplicateWebhookUrl(getInstance(), url)) {
            facesMessages.addGlobal(SEVERITY_ERROR, msgs.format("jsf.project.DuplicateUrl", url));
            return;
        }
        boolean isAdded = projectServiceImpl.addWebhook(getInstance(), url, secret, name, types);
        if (isAdded) {
            facesMessages.addGlobal(msgs.format("jsf.project.AddNewWebhook", url));
        }
    }

    @Transactional
    public void removeWebHook(String id) {
        identity.checkPermission(getInstance(), "update");
        WebHook webHook = webHookDAO.findById(Long.valueOf(id));
        if (webHook != null) {
            String url = webHook.getUrl();
            getInstance().getWebHooks().remove(webHook);
            webHookDAO.makeTransient(webHook);
            facesMessages.addGlobal(msgs.format("jsf.project.RemoveWebhook", url));
        }
    }

    @Transactional
    public void updateWebhook(String id, String url, String secret, String strTypes, String name) {
        identity.checkPermission(getInstance(), "update");
        Set<WebhookType> types = getTypesFromString(strTypes);
        if (types.isEmpty()) {
            facesMessages.addGlobal(msgs.get("jsf.project.webhookType.empty"));
            return;
        }
        if (!isValidUrl(url)) {
            return;
        }
        Long webhookId = Long.valueOf(id);
        if (projectServiceImpl.isDuplicateWebhookUrl(getInstance(), url, webhookId)) {
            facesMessages.addGlobal(SEVERITY_ERROR, msgs.format("jsf.project.DuplicateUrl", url));
            return;
        }
        boolean updated = projectServiceImpl.updateWebhook(getInstance(), webhookId, url, secret, name, types);
        if (updated) {
            facesMessages.addGlobal(msgs.format("jsf.project.UpdateWebhook", url));
        }
    }

    public void testWebhook(String url, String secret) {
        identity.checkPermission(getInstance(), "update");
        if (projectServiceImpl.isDuplicateWebhookUrl(getInstance(), url)) {
            facesMessages.addGlobal(SEVERITY_ERROR, msgs.format("jsf.project.DuplicateUrl", url));
            return;
        }
        if (!isValidUrl(url)) {
            return;
        }
        webhookServiceImpl.processTestEvent(identity.getAccountUsername(), getSlug(), url, secret);
    }

    /**
     * Check if url is valid and there is no duplication of url+type
     */
    private boolean isValidUrl(String url) {
        if (!webhookServiceImpl.isValidUrl(url)) {
            facesMessages.addGlobal(SEVERITY_ERROR, msgs.format("jsf.project.InvalidUrl", url));
            return false;
        }
        return true;
    }

    /**
     * If this action is enabled(Warning or Error), then it's exclusive
     * validation will be turn off
     *
     * @param selectedValidationAction
     */
    private void ensureMutualExclusivity(ValidationAction selectedValidationAction) {
        if (selectedValidationAction.getState() != ValidationAction.State.Off) {
            for (ValidationAction exclusiveValAction : selectedValidationAction.getExclusiveValidations()) {
                getInstance().getCustomizedValidations().put(exclusiveValAction.getId().name(),
                        ValidationAction.State.Off.name());
                getValidations().get(exclusiveValAction.getId()).setState(ValidationAction.State.Off);
            }
        }
    }

    public List<ValidationAction.State> getValidationStates() {
        return Arrays.asList(ValidationAction.State.values());
    }

    /**
     * Update the about page to the entered value, and show a success message.
     */
    @Transactional
    public void updateAboutPage() {
        identity.checkPermission(getInstance(), "update");
        String status = update();
        if ("updated".equals(status)) {
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO, msgs.get("jsf.project.AboutPageUpdated"));
        } else {
            facesMessages.addGlobal(FacesMessage.SEVERITY_ERROR, msgs.get("jsf.project.AboutPageUpdateFailed"));
        }
    }

    @Override
    protected void updatedMessage() {
        // Disable the default message from Seam
    }

    @ViewScoped
    public static class ProjectMaintainersAutocomplete extends MaintainerAutocomplete {

        private static final long serialVersionUID = -6765972032876700000L;
        @Inject
        private ProjectHome projectHome;
        @Inject
        private WebhookServiceImpl webhookServiceImpl;
        @Inject
        private Messages msgs;
        @Inject
        private ZanataIdentity zanataIdentity;
        @Inject
        private PersonDAO personDAO;
        @Inject
        private FacesMessages facesMessages;

        private HProject getInstance() {
            return projectHome.getInstance();
        }

        @Override
        protected List<HPerson> getMaintainers() {
            List<HPerson> list = Lists.newArrayList(getInstance().getMaintainers());
            Collections.sort(list, ComparatorUtil.PERSON_NAME_COMPARATOR);
            return list;
        }

        /**
         * Action when an item is selected
         */
        @Override
        @Transactional
        public void onSelectItemAction() {
            if (StringUtils.isEmpty(getSelectedItem())) {
                return;
            }
            zanataIdentity.checkPermission(getInstance(), "update");
            HPerson maintainer = personDAO.findByUsername(getSelectedItem());
            getInstance().addMaintainer(maintainer);
            projectHome.update();
            reset();
            projectHome.getMaintainerFilter().reset();
            facesMessages.addGlobal(FacesMessage.SEVERITY_INFO,
                    msgs.format("jsf.project.MaintainerAdded", maintainer.getName()));
            webhookServiceImpl.processWebhookMaintainerChanged(getInstance().getSlug(),
                    maintainer.getAccount().getUsername(), Maintainer, getInstance().getWebHooks(),
                    ProjectMaintainerChangedEvent.ChangeType.ADD);
        }
    }

    public List<ProjectType> getProjectTypeList() {
        List<ProjectType> projectTypes = Arrays.asList(ProjectType.values());
        Collections.sort(projectTypes, ComparatorUtil.PROJECT_TYPE_COMPARATOR);
        return projectTypes;
    }

    public ProjectSlug getProjectSlug() {
        return this.projectSlug;
    }

    /**
     * This field is set from form input which can differ from original slug
     */
    @Nullable
    public String getInputSlugValue() {
        return this.inputSlugValue;
    }

    /**
     * This field is set from form input which can differ from original slug
     */
    public void setInputSlugValue(@Nullable final String inputSlugValue) {
        this.inputSlugValue = inputSlugValue;
    }

    public void setProjectId(final Long projectId) {
        this.projectId = projectId;
    }

    public Long getProjectId() {
        return this.projectId;
    }

    public String getEnabledLocalesFilter() {
        return this.enabledLocalesFilter;
    }

    public void setEnabledLocalesFilter(final String enabledLocalesFilter) {
        this.enabledLocalesFilter = enabledLocalesFilter;
    }

    public String getDisabledLocalesFilter() {
        return this.disabledLocalesFilter;
    }

    public void setDisabledLocalesFilter(final String disabledLocalesFilter) {
        this.disabledLocalesFilter = disabledLocalesFilter;
    }

    /**
     * A separate map is used, rather than binding the alias map from the
     * project directly. This is done so that empty values are not added to the
     * map in every form submission, and so that a value entered in the field
     * for a row is not automatically updated when a different row is submitted.
     */
    public Map<LocaleId, String> getEnteredLocaleAliases() {
        return this.enteredLocaleAliases;
    }

    /**
     * A separate map is used, rather than binding the alias map from the
     * project directly. This is done so that empty values are not added to the
     * map in every form submission, and so that a value entered in the field
     * for a row is not automatically updated when a different row is submitted.
     */
    public void setEnteredLocaleAliases(final Map<LocaleId, String> enteredLocaleAliases) {
        this.enteredLocaleAliases = enteredLocaleAliases;
    }

    public void setSelectedEnabledLocales(final Map<LocaleId, Boolean> selectedEnabledLocales) {
        this.selectedEnabledLocales = selectedEnabledLocales;
    }

    public Map<LocaleId, Boolean> getSelectedDisabledLocales() {
        return this.selectedDisabledLocales;
    }

    public void setSelectedDisabledLocales(final Map<LocaleId, Boolean> selectedDisabledLocales) {
        this.selectedDisabledLocales = selectedDisabledLocales;
    }

    public Boolean getSelectedCheckbox() {
        return this.selectedCheckbox;
    }

    public void setSelectedCheckbox(final Boolean selectedCheckbox) {
        this.selectedCheckbox = selectedCheckbox;
    }

    public List<HProjectIteration> getVersions() {
        this.versions.compareAndSet(null, fetchVersions());
        return versions.get();
    }

    public String getSelectedProjectType() {
        return this.selectedProjectType;
    }

    public ProjectMaintainersAutocomplete getMaintainerAutocomplete() {
        return this.maintainerAutocomplete;
    }

    public AbstractListFilter<HPerson> getMaintainerFilter() {
        return this.maintainerFilter;
    }
}