org.opencms.i18n.CmsLocaleGroupService.java Source code

Java tutorial

Introduction

Here is the source code for org.opencms.i18n.CmsLocaleGroupService.java

Source

/*
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
 *
 * This library 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 library 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.
 *
 * For further information about Alkacon Software, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.i18n;

import org.opencms.ade.configuration.CmsADEManager;
import org.opencms.file.CmsObject;
import org.opencms.file.CmsProperty;
import org.opencms.file.CmsPropertyDefinition;
import org.opencms.file.CmsResource;
import org.opencms.file.CmsResourceFilter;
import org.opencms.lock.CmsLock;
import org.opencms.lock.CmsLockActionRecord;
import org.opencms.lock.CmsLockActionRecord.LockChange;
import org.opencms.lock.CmsLockUtil;
import org.opencms.main.CmsException;
import org.opencms.main.CmsLog;
import org.opencms.main.OpenCms;
import org.opencms.relations.CmsRelation;
import org.opencms.relations.CmsRelationFilter;
import org.opencms.relations.CmsRelationType;
import org.opencms.security.CmsPermissionSet;
import org.opencms.security.CmsSecurityException;
import org.opencms.site.CmsSite;
import org.opencms.util.CmsFileUtil;
import org.opencms.util.CmsStringUtil;
import org.opencms.util.CmsUUID;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.apache.commons.logging.Log;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

/**
 * Helper class for manipulating locale groups.<p>
 *
 * A locale group is a construct used to group pages which are translations of each other.
 *  *
 * A locale group consists of a set of resources connected by relations in the following way:<p>
 * <ul>
 * <li> There is a primary resource and a set of secondary resources.
 * <li> Each secondary resource has a relation to the primary resource of type LOCALE_VARIANT.
 * <li> Ideally, each resource has a different locale.
 * </ul>
 *
 * The point of the primary resource is to act as a 'master' resource which translators then use to translate to different locales.
 */
public class CmsLocaleGroupService {

    /**
     * Enum representing whether two resources can be linked together in a locale group.<p>
     */
    public enum Status {
        /** Resource already linked. */
        alreadyLinked,

        /** Resource linkable to locale group.*/
        linkable,

        /** Resource to link has a locale which is marked as 'do not translate' on the locale group. */
        notranslation,

        /** Other reason that resource can't be linked to locale group. */
        other
    }

    /** The logger instance for this class. */
    private static final Log LOG = CmsLog.getLog(CmsLocaleGroupService.class);

    /** CMS context to use for VFS operations. */
    private CmsObject m_cms;

    /**
     * Creates a new instance.<p>
     *
     * @param cms the CMS context to use
     */
    public CmsLocaleGroupService(CmsObject cms) {
        m_cms = cms;
    }

    /**
     * Helper method for getting the possible locales for a resource.<p>
     *
     * @param cms the CMS context
     * @param currentResource the resource
     *
     * @return the possible locales for a resource
     */
    public static List<Locale> getPossibleLocales(CmsObject cms, CmsResource currentResource) {

        CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(currentResource.getRootPath());
        List<Locale> secondaryLocales = Lists.newArrayList();
        Locale mainLocale = null;
        if (site != null) {
            List<Locale> siteLocales = site.getSecondaryTranslationLocales();
            mainLocale = site.getMainTranslationLocale(null);
            if ((siteLocales == null) || siteLocales.isEmpty()) {
                siteLocales = OpenCms.getLocaleManager().getAvailableLocales();
                if (mainLocale == null) {
                    mainLocale = siteLocales.get(0);
                }
            }
            secondaryLocales.addAll(siteLocales);
        }

        try {
            CmsProperty secondaryLocaleProp = cms.readPropertyObject(currentResource,
                    CmsPropertyDefinition.PROPERTY_SECONDARY_LOCALES, true);
            String propValue = secondaryLocaleProp.getValue();
            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(propValue)) {
                List<Locale> restrictionLocales = Lists.newArrayList();
                String[] tokens = propValue.trim().split(" *, *"); //$NON-NLS-1$
                for (String token : tokens) {
                    OpenCms.getLocaleManager();
                    Locale localeForToken = CmsLocaleManager.getLocale(token);
                    restrictionLocales.add(localeForToken);
                }
                if (!restrictionLocales.isEmpty()) {
                    secondaryLocales.retainAll(restrictionLocales);
                }
            }
        } catch (CmsException e) {
            LOG.error(e.getLocalizedMessage(), e);
        }
        List<Locale> result = new ArrayList<Locale>();
        result.add(mainLocale);
        for (Locale secondaryLocale : secondaryLocales) {
            if (!result.contains(secondaryLocale)) {
                result.add(secondaryLocale);
            }
        }
        return result;
    }

    /**
     * Adds a resource to a locale group.<p>
     *
     * Note: This is a low level method that is hard to use correctly. Please use attachLocaleGroupIndirect if at all possible.
     *
     * @param secondaryPage the page to add
     * @param primaryPage the primary resource of the locale group which the resource should be added to
     * @throws CmsException if something goes wrong
     */
    public void attachLocaleGroup(CmsResource secondaryPage, CmsResource primaryPage) throws CmsException {

        if (secondaryPage.getStructureId().equals(primaryPage.getStructureId())) {
            throw new IllegalArgumentException(
                    "A page can not be linked with itself as a locale variant: " + secondaryPage.getRootPath());
        }
        CmsLocaleGroup group = readLocaleGroup(secondaryPage);
        if (group.isRealGroup()) {
            throw new IllegalArgumentException(
                    "The page " + secondaryPage.getRootPath() + " is already part of a group. ");
        }

        // TODO: Check for redundant locales

        CmsLocaleGroup targetGroup = readLocaleGroup(primaryPage);
        CmsLockActionRecord record = CmsLockUtil.ensureLock(m_cms, secondaryPage);
        try {
            m_cms.deleteRelationsFromResource(secondaryPage,
                    CmsRelationFilter.ALL.filterType(CmsRelationType.LOCALE_VARIANT));
            m_cms.addRelationToResource(secondaryPage, targetGroup.getPrimaryResource(),
                    CmsRelationType.LOCALE_VARIANT.getName());
        } finally {
            if (record.getChange() == LockChange.locked) {
                m_cms.unlockResource(secondaryPage);
            }
        }
    }

    /**
     * Smarter method to connect a resource to a locale group.<p>
     *
     * Exactly one of the resources given as an argument must represent a locale group, while the other should
     * be the locale that you wish to attach to the locale group.<p>
     *
     * @param first a resource
     * @param second a resource
     * @throws CmsException if something goes wrong
     */
    public void attachLocaleGroupIndirect(CmsResource first, CmsResource second) throws CmsException {

        CmsResource firstResourceCorrected = getDefaultFileOrSelf(first);
        CmsResource secondResourceCorrected = getDefaultFileOrSelf(second);
        if ((firstResourceCorrected == null) || (secondResourceCorrected == null)) {
            throw new IllegalArgumentException("no default file");
        }

        CmsLocaleGroup group1 = readLocaleGroup(firstResourceCorrected);
        CmsLocaleGroup group2 = readLocaleGroup(secondResourceCorrected);
        int numberOfRealGroups = (group1.isRealGroupOrPotentialGroupHead() ? 1 : 0)
                + (group2.isRealGroupOrPotentialGroupHead() ? 1 : 0);
        if (numberOfRealGroups != 1) {
            throw new IllegalArgumentException("more than one real groups");
        }
        CmsResource main = null;
        CmsResource secondary = null;
        if (group1.isRealGroupOrPotentialGroupHead()) {
            main = group1.getPrimaryResource();
            secondary = group2.getPrimaryResource();
        } else if (group2.isRealGroupOrPotentialGroupHead()) {
            main = group2.getPrimaryResource();
            secondary = group1.getPrimaryResource();
        }
        attachLocaleGroup(secondary, main);
    }

    /**
     * Checks if the two resources are linkable as locale variants and returns an appropriate status<p>
     *
     *  This is the case if exactly one of the resources represents a locale group, the locale of the other resource
     *  is not already present in the locale group, and if some other permission / validity checks are passed.
     *
     * @param firstResource a resource
     * @param secondResource a resource
     *
     * @return the result of the linkability check
     */
    public Status checkLinkable(CmsResource firstResource, CmsResource secondResource) {

        String debugPrefix = "checkLinkable [" + Thread.currentThread().getName() + "]: ";
        LOG.debug(debugPrefix + (firstResource != null ? firstResource.getRootPath() : null) + " -- "
                + (secondResource != null ? secondResource.getRootPath() : null));
        try {
            CmsResource firstResourceCorrected = getDefaultFileOrSelf(firstResource);
            CmsResource secondResourceCorrected = getDefaultFileOrSelf(secondResource);
            if ((firstResourceCorrected == null) || (secondResourceCorrected == null)) {
                LOG.debug(debugPrefix + " rejected - no resource");
                return Status.other;
            }
            Locale locale1 = OpenCms.getLocaleManager().getDefaultLocale(m_cms, firstResourceCorrected);
            Locale locale2 = OpenCms.getLocaleManager().getDefaultLocale(m_cms, secondResourceCorrected);
            if (locale1.equals(locale2)) {
                LOG.debug(debugPrefix + "  rejected - same locale " + locale1);
                return Status.other;
            }

            Locale mainLocale1 = getMainLocale(firstResourceCorrected.getRootPath());
            Locale mainLocale2 = getMainLocale(secondResourceCorrected.getRootPath());
            if ((mainLocale1 == null) || !(mainLocale1.equals(mainLocale2))) {
                LOG.debug(debugPrefix + " rejected - incompatible main locale " + mainLocale1 + "/" + mainLocale2);
                return Status.other;
            }

            CmsLocaleGroup group1 = readLocaleGroup(firstResourceCorrected);
            Set<Locale> locales1 = group1.getLocales();
            CmsLocaleGroup group2 = readLocaleGroup(secondResourceCorrected);
            Set<Locale> locales2 = group2.getLocales();
            if (!(Sets.intersection(locales1, locales2).isEmpty())) {
                LOG.debug(debugPrefix + "  rejected - already linked (case 1)");
                return Status.alreadyLinked;
            }

            if (group1.isMarkedNoTranslation(group2.getLocales())
                    || group2.isMarkedNoTranslation(group1.getLocales())) {
                LOG.debug(debugPrefix + "  rejected - marked 'no translation'");
                return Status.notranslation;
            }

            if (group1.isRealGroupOrPotentialGroupHead() == group2.isRealGroupOrPotentialGroupHead()) {
                LOG.debug(debugPrefix + "  rejected - incompatible locale group states");
                return Status.other;
            }

            CmsResource permCheckResource = null;
            if (group1.isRealGroupOrPotentialGroupHead()) {
                permCheckResource = group2.getPrimaryResource();
            } else {
                permCheckResource = group1.getPrimaryResource();
            }
            if (!m_cms.hasPermissions(permCheckResource, CmsPermissionSet.ACCESS_WRITE, false,
                    CmsResourceFilter.IGNORE_EXPIRATION)) {
                LOG.debug(debugPrefix + " no write permissions: " + permCheckResource.getRootPath());
                return Status.other;
            }

            if (!checkLock(permCheckResource)) {
                LOG.debug(debugPrefix + " lock state: " + permCheckResource.getRootPath());
                return Status.other;
            }

            if (group2.getPrimaryResource().getStructureId().equals(group1.getPrimaryResource().getStructureId())) {
                LOG.debug(debugPrefix + "  rejected - already linked (case 2)");
                return Status.alreadyLinked;
            }
        } catch (Exception e) {
            LOG.error(debugPrefix + e.getLocalizedMessage(), e);
            LOG.debug(debugPrefix + "  rejected - exception (see previous)");
            return Status.other;
        }
        LOG.debug(debugPrefix + " OK");
        return Status.linkable;
    }

    /**
     * Removes a locale group relation between two resources.<p>
     *
     * @param firstPage the first resource
     * @param secondPage the second resource
     * @throws CmsException if something goes wrong
     */
    public void detachLocaleGroup(CmsResource firstPage, CmsResource secondPage) throws CmsException {

        CmsRelationFilter typeFilter = CmsRelationFilter.ALL.filterType(CmsRelationType.LOCALE_VARIANT);
        firstPage = getDefaultFileOrSelf(firstPage);
        secondPage = getDefaultFileOrSelf(secondPage);
        if ((firstPage == null) || (secondPage == null)) {
            return;
        }

        List<CmsRelation> relations = m_cms
                .readRelations(typeFilter.filterStructureId(secondPage.getStructureId()));
        CmsUUID firstId = firstPage.getStructureId();
        CmsUUID secondId = secondPage.getStructureId();
        for (CmsRelation relation : relations) {
            CmsUUID sourceId = relation.getSourceId();
            CmsUUID targetId = relation.getTargetId();
            CmsResource resourceToModify = null;
            if (sourceId.equals(firstId) && targetId.equals(secondId)) {
                resourceToModify = firstPage;
            } else if (sourceId.equals(secondId) && targetId.equals(firstId)) {
                resourceToModify = secondPage;
            }
            if (resourceToModify != null) {
                CmsLockActionRecord record = CmsLockUtil.ensureLock(m_cms, resourceToModify);
                try {
                    m_cms.deleteRelationsFromResource(resourceToModify, typeFilter);
                } finally {
                    if (record.getChange() == LockChange.locked) {
                        m_cms.unlockResource(resourceToModify);
                    }
                }
                break;
            }
        }
    }

    /**
     * Tries to find the 'best' localized subsitemap parent folder for a resource.<p>
     *
     * This is used when we use locale group dialogs outside the sitemap editor, so we
     * don't have a clearly defined 'root resource' - this method is used to find a replacement
     * for the root resource which we would have in the sitemap editor.
     *
     * @param resource the resource for which to find the localization root
     * @return the localization root
     *
     * @throws CmsException if something goes wrong
     */
    public CmsResource findLocalizationRoot(CmsResource resource) throws CmsException {

        String rootPath = resource.getRootPath();
        LOG.debug("Trying to find localization root for " + rootPath);
        if (resource.isFile()) {
            rootPath = CmsResource.getParentFolder(rootPath);
        }
        CmsObject cms = OpenCms.initCmsObject(m_cms);
        cms.getRequestContext().setSiteRoot("");
        String currentPath = rootPath;
        CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(rootPath);
        if (site == null) {
            return null;
        }
        String siteroot = site.getSiteRoot();
        List<String> ancestors = Lists.newArrayList();
        while ((currentPath != null) && CmsStringUtil.isPrefixPath(siteroot, currentPath)) {
            ancestors.add(currentPath);
            currentPath = CmsResource.getParentFolder(currentPath);
        }
        Iterator<String> iter = ancestors.iterator();
        while (iter.hasNext()) {
            String path = iter.next();
            if (CmsFileUtil.removeTrailingSeparator(path).equals(CmsFileUtil.removeTrailingSeparator(siteroot))) {
                LOG.debug("keeping path because it is the site root: " + path);
            } else if (cms.existsResource(CmsStringUtil.joinPaths(path, CmsADEManager.CONFIG_SUFFIX),
                    CmsResourceFilter.IGNORE_EXPIRATION)) {
                LOG.debug("keeping path because it is a sub-sitemap: " + path);
            } else {
                LOG.debug("removing path " + path);
                iter.remove();
            }
        }
        for (String ancestor : ancestors) {
            try {
                CmsResource ancestorRes = cms.readResource(ancestor, CmsResourceFilter.IGNORE_EXPIRATION);
                CmsLocaleGroup group = readLocaleGroup(ancestorRes);
                if (group.isRealGroup()) {
                    return ancestorRes;
                }
            } catch (CmsException e) {
                LOG.error(e.getLocalizedMessage(), e);
            }
        }
        // there is at least one ancestor: the site root
        String result = ancestors.get(0);
        LOG.debug("result = " + result);
        return cms.readResource(result, CmsResourceFilter.IGNORE_EXPIRATION);

    }

    /**
     * Gets the main translation locale configured for the given root path.<p>
     *
     * @param rootPath a root path
     *
     * @return the main translation locale configured for the given path, or null if none was found
     */
    public Locale getMainLocale(String rootPath) {

        CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(rootPath);
        if (site == null) {
            return null;
        }
        return site.getMainTranslationLocale(null);
    }

    /**
     * Reads the locale group of a default file.<p>
     *
     * @param resource a resource which might be a folder or a file
     * @return the locale group corresponding to the default file
     *
     * @throws CmsException if something goes wrong
     */
    public CmsLocaleGroup readDefaultFileLocaleGroup(CmsResource resource) throws CmsException {

        if (resource.isFolder()) {
            CmsResource defaultFile = m_cms.readDefaultFile(resource, CmsResourceFilter.IGNORE_EXPIRATION);
            if (defaultFile != null) {
                return readLocaleGroup(defaultFile);
            } else {
                LOG.warn("default file not found, reading locale group of folder.");
            }
        }
        return readLocaleGroup(resource);

    }

    /**
     * Reads a locale group from the VFS.<p>
     *
     * @param resource the resource for which to read the locale group
     *
     * @return the locale group for the resource
     * @throws CmsException if something goes wrong
     */
    public CmsLocaleGroup readLocaleGroup(CmsResource resource) throws CmsException {

        if (resource.isFolder()) {
            CmsResource defaultFile = m_cms.readDefaultFile(resource, CmsResourceFilter.IGNORE_EXPIRATION);
            if (defaultFile != null) {
                resource = defaultFile;
            }
        }

        List<CmsRelation> relations = m_cms.readRelations(CmsRelationFilter.ALL
                .filterType(CmsRelationType.LOCALE_VARIANT).filterStructureId(resource.getStructureId()));
        List<CmsRelation> out = Lists.newArrayList();
        List<CmsRelation> in = Lists.newArrayList();
        for (CmsRelation rel : relations) {
            if (rel.getSourceId().equals(resource.getStructureId())) {
                out.add(rel);
            } else {
                in.add(rel);
            }
        }
        CmsResource primaryResource = null;
        List<CmsResource> secondaryResources = Lists.newArrayList();
        if ((out.size() == 0) && (in.size() == 0)) {
            primaryResource = resource;
        } else if ((out.size() == 0) && (in.size() > 0)) {
            primaryResource = resource;
            // resource is the primary variant
            for (CmsRelation relation : in) {
                CmsResource source = relation.getSource(m_cms, CmsResourceFilter.ALL);
                secondaryResources.add(source);
            }
        } else if ((out.size() == 1) && (in.size() == 0)) {

            CmsResource target = out.get(0).getTarget(m_cms, CmsResourceFilter.ALL);
            primaryResource = target;
            CmsRelationFilter filter = CmsRelationFilter.TARGETS.filterType(CmsRelationType.LOCALE_VARIANT)
                    .filterStructureId(target.getStructureId());
            List<CmsRelation> relationsToTarget = m_cms.readRelations(filter);
            for (CmsRelation targetRelation : relationsToTarget) {
                CmsResource secondaryResource = targetRelation.getSource(m_cms, CmsResourceFilter.ALL);
                secondaryResources.add(secondaryResource);
            }
        } else {
            throw new IllegalStateException("illegal locale variant relations for resource with id="
                    + resource.getStructureId() + ", path=" + resource.getRootPath());
        }
        return new CmsLocaleGroup(m_cms, primaryResource, secondaryResources);
    }

    /**
     * Helper method for reading the default file of a folder.<p>
     *
     * If the resource given already is a file, it will be returned, otherwise
     * the default file (or null, if none exists) of the folder will be returned.
     *
     * @param res the resource whose default file to read
     * @return the default file
     */
    protected CmsResource getDefaultFileOrSelf(CmsResource res) {

        CmsResource defaultfile = null;
        if (res.isFolder()) {
            try {
                defaultfile = m_cms.readDefaultFile("" + res.getStructureId());
            } catch (CmsSecurityException e) {
                LOG.error(e.getLocalizedMessage(), e);
                return null;
            } catch (CmsException e) {
                LOG.error(e.getLocalizedMessage(), e);
                return null;
            }
            return defaultfile;
        }
        return res;

    }

    /**
     * Checks that the resource is not locked by another user.<p>
     *
     * @param resource the resource
     * @return true if the resource is not locked by another user
     *
     * @throws CmsException if something goes wrong
     */
    private boolean checkLock(CmsResource resource) throws CmsException {

        CmsLock lock = m_cms.getLock(resource);
        return lock.isUnlocked() || lock.getUserId().equals(m_cms.getRequestContext().getCurrentUser().getId());
    }

}