/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program 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
* of the License, or (at your option) any later version.
*
* This program 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 program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.common.impl.language;
import static ch.entwine.weblounge.common.language.Localizable.LanguageResolution.Default;
import static ch.entwine.weblounge.common.language.Localizable.LanguageResolution.Original;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.language.Localizable;
import ch.entwine.weblounge.common.language.LocalizationListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* This class represents a basic implementation of a {@link Localizable} object
* that brings support for all the language handling and dealing with language
* resolution strategies.
* <p>
* To keep the implementation simple, localized content can only be returned as
* a <code>String</code> using {@link #toString()}, {@link #toString(Language)}
* or {@link #toString(Language, boolean)}. To manage more complex content, it
* might be worth looking at {@link LocalizableContent}.
* <p>
* To be able to properly handle this object it should be noted that every
* localizable content will have an <i>original</i> language, a
* <code>current language</code> and possibly also a <i>default language</i>.
* <ul>
* <li><b>Original language</b>: the original language is automatically set as
* soon as localized content is added and thus represents the language of that
* first content.</li>
* <li><b>Current language</b>: the current language is what the object has been
* set to using the <code>switchTo(Language)</code> method. If that method has
* never been called, then the current language is either the original language
* or the default one if it has been set. In any case, as long as there is
* <i>some</code> content in the object, the current language will always be
* part of what is returned by <code>getSupportedLanguages()</code>, i. e.
* <code>supportsLanguage(currentLanguage)</code> will be true as long as
* <code>getCurrentLanguage()</code> does not return <code>null</code>.</li>
* <li><b>Default language</b>: The default language can be specified on a
* localized object as needed and yields an alternative way of getting content
* that is not available in the requested language.</li>
* </ul>
*
* @see LocalizableContent
*/
public class LocalizableObject implements Localizable {
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(LocalizableObject.class);
/** list of added languages */
protected Set<Language> languages = null;
/** the language that has been provided first */
protected Language originalLanguage = null;
/** the default language */
protected Language defaultLanguage = null;
/** the language that is currently set for this object */
protected Language currentLanguage = null;
/** the selector for the default language selection */
protected LanguageResolution behavior = Original;
/** list of objects interested in language switches */
protected List<LocalizationListener> localizationListeners = null;
/**
* Creates a new localizable object with a default behavior of
* {@link LanguageBehaviour#Original}.
*/
public LocalizableObject() {
this(null);
}
/**
* Creates localizable object with a default language <code>language</code>
* and the behavior set to {@link LanguageResolution#Default} as long as
* <code>language</code> is not set to <code>null</code>, in which case the
* behavior will be set to {@link LanguageResolution#Original}.
*
* @param identifier
* the default language
*/
public LocalizableObject(Language defaultLanguage) {
this.defaultLanguage = defaultLanguage;
languages = new TreeSet<Language>();
behavior = (defaultLanguage != null) ? Default : Original;
localizationListeners = new ArrayList<LocalizationListener>();
}
/**
* Adds the listener to the list of localization listeners. The listeners will
* be notified as soon as a call to {@link #switchTo(Language)} or
* {@link #switchTo(Language, boolean)} has been made.
*
* @param listener
* the listener to add
*/
public void addLocalizationListener(LocalizationListener listener) {
synchronized (localizationListeners) {
localizationListeners.add(listener);
}
}
/**
* Removes the listener from the list of localization listeners.
*
* @param listener
* the listener to remove
*/
public void removeLocalizationListener(LocalizationListener listener) {
synchronized (localizationListeners) {
localizationListeners.remove(listener);
}
}
/**
* Resets all language settings, including original, default, selected and
* active language.
*/
protected void reset() {
originalLanguage = null;
defaultLanguage = null;
currentLanguage = null;
languages.clear();
}
/**
* Enables the given language for this object.
*
* @param language
* the language to enable
*/
public void enableLanguage(Language language) {
if (language == null)
throw new IllegalArgumentException("Language must not be null");
if (!languages.contains(language)) {
languages.add(language);
if (languages.size() == 1) {
originalLanguage = language;
currentLanguage = language;
}
}
}
/**
* Removes the given language from this object.
*
* @param language
* the language to be removed
*/
public void disableLanguage(Language language) {
if (language == null)
throw new IllegalArgumentException("Language must not be null");
// Handle the current language
if (language.equals(currentLanguage))
currentLanguage = null;
// Remove the language in question
languages.remove(language);
// Fix original language
if (behavior.equals(Original) && language.equals(originalLanguage)) {
if (languages.size() > 0) {
defaultLanguage = languages.iterator().next();
behavior = Default;
logger.trace("Switching localizable object {} from original language {} to default language {}", new Object[] {
this,
originalLanguage,
defaultLanguage });
}
originalLanguage = null;
}
// Fix default language
else if (behavior.equals(Default) && language.equals(defaultLanguage)) {
if (languages.size() > 0) {
defaultLanguage = languages.iterator().next();
logger.trace("Switching default language of localizable object {} to {}", this, defaultLanguage);
} else {
defaultLanguage = null;
}
}
}
/**
* Makes <code>language</code> the currently selected language for this
* object.
*
* @param language
* the language to switch to
* @throws IllegalArgumentException
* if the language argument is <code>null</code>
* @throws IllegalStateException
* if no fall back language can be determined
*/
public Language switchTo(Language language) {
return switchTo(language, false);
}
/**
* Makes <code>language</code> the currently selected language for this
* object.
*
* @param language
* the language to switch to
* @param force
* force the language and don't fall back to either original or
* default language
* @throws IllegalArgumentException
* if the language argument is <code>null</code>
* @throws IllegalStateException
* if <code>force</code> is <code>false</code> and no fall back
* language can be determined
*/
public Language switchTo(Language language, boolean force) {
if (language == null)
throw new IllegalArgumentException("Language must not be null");
// If this object does not care about language switching, we don't care either
if (languages.size() == 0)
return language;
// Remember the current language
Language original = currentLanguage;
// If the language is available, then simply select it
if (supportsLanguage(language)) {
currentLanguage = language;
}
// If the language is forced but not available, then throw an exception
else if (force) {
throw new IllegalStateException(this + " is not localized to " + language.getLocale().getDisplayLanguage());
}
// The selected language is not available. Use the original language instead
else if (behavior.equals(Original)) {
if (getOriginalLanguage() != null)
currentLanguage = getOriginalLanguage();
else if (languages.size() > 0)
throw new IllegalStateException("Language resolution failed for " + this);
}
// The selected language is not available. Use the default language instead
else if (behavior.equals(Default)) {
if (getDefaultLanguage() != null)
currentLanguage = getDefaultLanguage();
}
// Check the resolution process outcome
if (currentLanguage == null && languages.size() > 0)
throw new IllegalStateException("Language resolution failed for " + this);
// Notify interested parties
if (original == null || !original.equals(currentLanguage)) {
fireLanguageChanged(currentLanguage, language);
}
return currentLanguage;
}
/**
* Notifies the registered localization listeners about the newly selected
* language.
*
* @param language
* the language that has been switched to
* @param requested
* the language that was originally requested
*/
protected void fireLanguageChanged(Language language, Language requested) {
synchronized (localizationListeners) {
for (LocalizationListener l : localizationListeners) {
l.switchedTo(language, requested);
}
}
}
/**
* Returns the language that is currently used to display the object.
*
* @return the currently active language
*/
public Language getLanguage() {
return currentLanguage;
}
/**
* Returns a fall back language according to the current
* {@link LanguageResolution}.
*
* @return the fall back language
*/
protected Language resolveLanguage() {
if (behavior.equals(Original) && originalLanguage != null)
return originalLanguage;
else if (behavior.equals(Default) && defaultLanguage != null)
return defaultLanguage;
else
throw new IllegalStateException("Language resolution failed");
}
/**
* Sets the behavior of this localizable object in the case that the object
* description is requested in a language that has not been provided.<br>
* There are two known behaviors:
* <ul>
* <li>{@link LanguageResolution#Original}: If the requested language is not
* in the list of supported languages, then the language is chosen that was
* used to first create this object.</li>
* <li>{@link LanguageResolution#Default}: If the requested language is not in
* the list of supported languages, then the language is chosen that was set
* using {@link #setDefaultLanguage(Language)}.
* </ul>
* The default for this setting is {@link LanguageResolution#Original}.
*
* @param behavior
* the language behavior
* @throws IllegalStateException
* if a default language has not been specified but
* {@link LanguageResolution#Default} has been chosen for language
* resolution
*/
public void setLanguageResolution(LanguageResolution behavior)
throws IllegalStateException {
if (Default.equals(behavior) && defaultLanguage == null)
throw new IllegalStateException("Must specify default language first");
if (Original.equals(behavior) && originalLanguage == null && languages.size() > 0)
throw new IllegalStateException("Must specify original language first");
this.behavior = behavior;
}
/**
* Returns the behavior of this multilingual object regarding it's default
* language.
*
* @return the default language behavior
* @see #setLanguageResolution(int)
*/
public LanguageResolution getLanguageResolution() {
return behavior;
}
/**
* Explicitly sets the default language.
* <p>
* If the language has not yet been enabled, this method will enable it.
*
* @param language
* the default language
* @throws IllegalArgumentException
* if the argument <code>language</code> was null
*/
public void setDefaultLanguage(Language language) {
if (language == null && behavior.equals(Default))
throw new IllegalArgumentException("Default language may not be null while language resolution is set to default");
defaultLanguage = language;
}
/**
* Returns the default language.
*
* @return the default language for this object
*/
public Language getDefaultLanguage() {
return defaultLanguage;
}
/**
* Returns the original language of this object or <code>null</code>, if no
* original language exists.
*
* @return the original language
*/
public Language getOriginalLanguage() {
return originalLanguage;
}
/**
* Sets the original language for this object. The original language is
* considered to be the language that was first used to describe the object
* (the native language).
* <p>
* If the language has not yet been enabled, this method will enable it.
*
* @param language
* the original language for this object
* @throws IllegalArgumentException
* if the argument <code>language</code> was null
*/
public void setOriginalLanguage(Language language) {
if (language == null)
throw new IllegalArgumentException("Language must not be null!");
originalLanguage = language;
}
/**
* Returns <code>true</code> if the given language is supported, i. e. if the
* language has been added using <code>addLanguage()</code>.
*
* @param language
* the language to be supported
* @return <code>true</code> if the language is supported
*/
public boolean supportsLanguage(Language language) {
if (language == null)
throw new IllegalArgumentException("Language must not be null");
return languages.contains(language);
}
/**
* Returns the languages currently supported by this object.
*
* @return a supported language iteration.
*/
public Set<Language> languages() {
return languages;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.language.Localizable#compareTo(ch.entwine.weblounge.common.language.Localizable)
*/
public int compareTo(Localizable o, Language l) {
if (o == null)
throw new IllegalArgumentException("Localizable must not be null");
if (l == null)
throw new IllegalArgumentException("Language must not be null");
if (o instanceof LocalizableObject) {
return toString(l).compareTo(((LocalizableObject) o).toString(l));
}
return toString(l).compareTo(o.toString());
}
/**
* Returns the string representation in the current language.
* <p>
* This implementation forwards the request to
* {@link #toString(Language, boolean)}.
*
* @return the component title.
*/
public String toString() {
return toString(resolveLanguage(), false);
}
/**
* Returns the string representation in the requested language or
* <code>null</code> if the title doesn't exist in that language.
* <p>
* This implementation forwards the request to
* {@link #toString(Language, boolean)}.
*
* @param language
* the requested language
* @return the object title
*/
public String toString(Language language) {
return toString(language, false);
}
/**
* Returns the string representation in the specified language. If no content
* can be found in that language, then it will be looked up in the default
* language (unless <code>force</code> is set to <code>true</code>). <br>
* If this doesn't produce a result as well, <code>null</code> is returned.
*
* @param language
* the language
* @param force
* <code>true</code> to force the language
* @return the object's string representation in the given language
*/
public String toString(Language language, boolean force) {
return super.toString();
}
}