/** * This file Copyright (c) 2008-2012 Magnolia International * Ltd. (http://www.magnolia-cms.com). All rights reserved. * * * This file is dual-licensed under both the Magnolia * Network Agreement and the GNU General Public License. * You may elect to use one or the other of these licenses. * * This file is distributed in the hope that it will be * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. * Redistribution, except as permitted by whichever of the GPL * or MNA you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or * modify this file under the terms of the GNU General * Public License, Version 3, as published by the Free Software * Foundation. You should have received a copy of the GNU * General Public License, Version 3 along with this program; * if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * 2. For the Magnolia Network Agreement (MNA), this file * and the accompanying materials are made available under the * terms of the MNA which accompanies this distribution, and * is available at http://www.magnolia-cms.com/mna.html * * Any modifications to this file must keep this entire header * intact. * */ package info.magnolia.cms.i18n; import info.magnolia.cms.core.Content; import info.magnolia.cms.core.NodeData; import info.magnolia.cms.util.NodeDataUtil; import info.magnolia.context.MgnlContext; import info.magnolia.jcr.util.PropertyUtil; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.jcr.Node; import javax.jcr.Property; import javax.jcr.RepositoryException; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An abstract implementation of {@link I18nContentSupport} which stores the * locale specific content in node data having a local suffix: * <name>_<locale>. * * The detection of the current locale, based on the URI for instance, is left to the concrete implementation. * @author philipp * */ public abstract class AbstractI18nContentSupport implements I18nContentSupport { private static final Logger log = LoggerFactory.getLogger(AbstractI18nContentSupport.class); /** * The content is served for this locale if the the content is not available for the current locale. */ private Locale fallbackLocale = new Locale("en"); /** * If no locale can be determined the default locale will be set. If no default locale is defined the fallback locale is used. */ protected Locale defaultLocale; private boolean enabled = false; /** * The active locales. */ private final Map<String, Locale> locales = new LinkedHashMap<String, Locale>(); @Override public Locale getLocale() { Locale locale = null; if(MgnlContext.getWebContextOrNull() != null){ locale = MgnlContext.getAggregationState().getLocale(); } if (locale == null) { return fallbackLocale; } return locale; } @Override public void setLocale(Locale locale) { MgnlContext.getAggregationState().setLocale(locale); } @Override public Locale getFallbackLocale() { return this.fallbackLocale; } @Override public void setFallbackLocale(Locale fallbackLocale) { this.fallbackLocale = fallbackLocale; } /** * Returns the closest locale for which {@link #isLocaleSupported(Locale)} is true. * <ul> * <li>If the locale has a country specified (fr_CH) the locale without country (fr) will be returned.</li> * <li>If the locale has no country specified (fr) the first locale with the same language but specific country (fr_CH) will be returned.</li> * <li>If this fails the fall-back locale is returned</li> * </ul> * Warning: if you have configured both (fr and fr_CH) this method will jiter between this two values. */ protected Locale getNextLocale(Locale locale) { // if this locale defines a country if(StringUtils.isNotEmpty(locale.getCountry())){ // try to use the language only Locale langOnlyLocale = new Locale(locale.getLanguage()); if(isLocaleSupported(langOnlyLocale)){ return langOnlyLocale; } } // try to find a locale with the same language (ignore the country) for (Iterator<Locale> iter = getLocales().iterator(); iter.hasNext();) { Locale otherCountryLocale = iter.next(); // same lang, but not the same country as well or we end up in the loop if(locale.getLanguage().equals(otherCountryLocale.getLanguage()) && !locale.equals(otherCountryLocale)){ return otherCountryLocale; } } return getFallbackLocale(); } /** * Extracts the language from the uri. */ @Override public Locale determineLocale() { Locale locale; locale = onDetermineLocale(); // depending on the implementation the returned local can be null (not defined) if(locale == null){ locale = getDefaultLocale(); } // if we have a locale but it is not supported we try to get the closest locale if(!isLocaleSupported(locale)){ locale = getNextLocale(locale); } // instead of returning the content fallback language // we are going to return the default locale which might differ if(locale.equals(getFallbackLocale())){ locale = getDefaultLocale(); } return locale; } protected abstract Locale onDetermineLocale(); protected static Locale determineLocalFromString(String localeStr) { if(StringUtils.isNotEmpty(localeStr)){ String[] localeArr = StringUtils.split(localeStr, "_"); if(localeArr.length ==1){ return new Locale(localeArr[0]); } else if(localeArr.length == 2){ return new Locale(localeArr[0],localeArr[1]); } } return null; } @Override public String toI18NURI(String uri) { if (!isEnabled()) { return uri; } Locale locale = getLocale(); if (isLocaleSupported(locale)) { return toI18NURI(uri, locale); } return uri; } protected abstract String toI18NURI(String uri, Locale locale); /** * Removes the prefix. */ @Override public String toRawURI(String i18nURI) { if (!isEnabled()) { return i18nURI; } Locale locale = getLocale(); if (isLocaleSupported(locale)) { return toRawURI(i18nURI, locale); } return i18nURI; } protected abstract String toRawURI(String i18nURI, Locale locale); @Override public NodeData getNodeData(Content node, String name, Locale locale) throws RepositoryException { String nodeDataName = name + "_" + locale; if (node.hasNodeData(nodeDataName)) { return node.getNodeData(nodeDataName); } return null; } /** * Returns the nodedata with the name <name>_<current language> or <name>_<fallback language> * otherwise returns <name>. */ @Override public NodeData getNodeData(Content node, String name) { NodeData nd = null; if (isEnabled()) { try { // test for the current language Locale locale = getLocale(); Set<Locale> checkedLocales = new HashSet<Locale>(); // getNextContentLocale() returns null once the end of the locale chain is reached while(locale != null){ nd = getNodeData(node, name, locale); if (!isEmpty(nd)) { return nd; } checkedLocales.add(locale); locale = getNextContentLocale(locale, checkedLocales); } } catch (RepositoryException e) { log.error("can't read i18n nodeData " + name + " from node " + node, e); } } // return the node data return node.getNodeData(name); } @Override public Node getNode(Node node, String name) throws RepositoryException { if (isEnabled()) { try { // test for the current language Locale locale = getLocale(); Set<Locale> checkedLocales = new HashSet<Locale>(); // getNextContentLocale() returns null once the end of the locale chain is reached while(locale != null){ String localeSpecificChildName = name + "_" + locale; if (node.hasNode(localeSpecificChildName)) { return node.getNode(localeSpecificChildName); } checkedLocales.add(locale); locale = getNextContentLocale(locale, checkedLocales); } } catch (RepositoryException e) { log.error("can't read i18n node " + name + " from node " + node, e); } } return node.getNode(name); } @Override public boolean hasProperty(Node node, String name) throws RepositoryException { if (!isEnabled()) { return node.hasProperty(name); } try { // get property using all the rules in getProperty method. If not found, then it doesn't exist. getProperty(node, name); } catch (RepositoryException e) { return false; } return true; } @Override public Property getProperty(Node node, String name) throws RepositoryException { if (!isEnabled()) { return node.getProperty(name); } try { // test for the current language Locale locale = getLocale(); Set<Locale> checkedLocales = new HashSet<Locale>(); // getNextContentLocale() returns null once the end of the locale chain is reached while (locale != null) { Property property = getProperty(node, name, locale); if (!isEmpty(property)) { return property; } checkedLocales.add(locale); locale = getNextContentLocale(locale, checkedLocales); } } catch (RepositoryException e) { log.error("can't read i18n nodeData " + name + " from node " + node, e); } // return the node data return node.getProperty(name); } @Override public Property getProperty(Node node, String name, Locale locale) throws RepositoryException { String propName = name + "_" + locale; if (node.hasProperty(propName)) { return node.getProperty(propName); } return null; } /** * Uses {@link #getNextLocale(Locale)} to find the next locale. If the returned locale is in the * checkedLocales set it falls back to the fall-back locale. If the fall-back locale itself is * passed to the method, the method returns null to signal the end of the chain. */ protected Locale getNextContentLocale(Locale locale, Set<Locale> checkedLocales) { if(locale.equals(getFallbackLocale())){ return null; } Locale candidate = getNextLocale(locale); if(!checkedLocales.contains(candidate)){ return candidate; } return getFallbackLocale(); } @Override public boolean isEnabled() { return this.enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } @Override public Collection<Locale> getLocales() { return this.locales.values(); } public void setLocales(Map<String, Locale> locales) { this.locales.putAll(locales); } public void addLocale(LocaleDefinition ld) { if (ld.isEnabled()) { this.locales.put(ld.getId(), ld.getLocale()); } } protected boolean isLocaleSupported(Locale locale) { return locale != null && locales.containsKey(locale.toString()); } /** * Checks if the nodedata field is empty. * * @deprecated since 4.5.4. Use {@link #isEmpty(Property)} instead. */ @Deprecated protected boolean isEmpty(NodeData nd) { if (nd != null && nd.isExist()) { // TODO use a better way to find out if it is empty return StringUtils.isEmpty(NodeDataUtil.getValueString(nd)); } return true; } /** * Checks if the property field is empty. */ protected boolean isEmpty(Property nd) { if (nd != null) { return StringUtils.isEmpty(PropertyUtil.getValueString(nd)); } return true; } @Override public Locale getDefaultLocale() { if(this.defaultLocale == null){ return getFallbackLocale(); } return this.defaultLocale; } public void setDefaultLocale(Locale defaultLocale) { this.defaultLocale = defaultLocale; } }