org.opencms.i18n.CmsLocaleGroupService Maven / Gradle / Ivy
Show all versions of opencms-core Show documentation
/*
* 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.
*
* 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:
*
* - There is a primary resource and a set of secondary resources.
*
- Each secondary resource has a relation to the primary resource of type LOCALE_VARIANT.
*
- Ideally, each resource has a different locale.
*
*
* 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.
*/
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.
*
* @param cms the CMS context to use
*/
public CmsLocaleGroupService(CmsObject cms) {
m_cms = cms;
}
/**
* Helper method for getting the possible locales for a resource.
*
* @param cms the CMS context
* @param currentResource the resource
*
* @return the possible locales for a resource
*/
public static List getPossibleLocales(CmsObject cms, CmsResource currentResource) {
CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(currentResource.getRootPath());
List secondaryLocales = Lists.newArrayList();
Locale mainLocale = null;
if (site != null) {
List 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 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 result = new ArrayList();
result.add(mainLocale);
for (Locale secondaryLocale : secondaryLocales) {
if (!result.contains(secondaryLocale)) {
result.add(secondaryLocale);
}
}
return result;
}
/**
* Adds a resource to a locale group.
*
* 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.
*
* 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.
*
* @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
*
* 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 locales1 = group1.getLocales();
CmsLocaleGroup group2 = readLocaleGroup(secondResourceCorrected);
Set 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.
*
* @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 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.
*
* 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 ancestors = Lists.newArrayList();
while ((currentPath != null) && CmsStringUtil.isPrefixPath(siteroot, currentPath)) {
ancestors.add(currentPath);
currentPath = CmsResource.getParentFolder(currentPath);
}
Iterator 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.
*
* @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.
*
* @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.
*
* @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 relations = m_cms.readRelations(
CmsRelationFilter.ALL.filterType(CmsRelationType.LOCALE_VARIANT).filterStructureId(
resource.getStructureId()));
List out = Lists.newArrayList();
List in = Lists.newArrayList();
for (CmsRelation rel : relations) {
if (rel.getSourceId().equals(resource.getStructureId())) {
out.add(rel);
} else {
in.add(rel);
}
}
CmsResource primaryResource = null;
List 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 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.
*
* 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.
*
* @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());
}
}