com.helger.commons.locale.LocaleCache Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ph-commons Show documentation
Show all versions of ph-commons Show documentation
Java 1.8+ Library with tons of utility classes required in all projects
/*
* Copyright (C) 2014-2024 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.helger.commons.locale;
import java.util.Locale;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.annotation.Singleton;
import com.helger.commons.collection.impl.CommonsHashSet;
import com.helger.commons.collection.impl.CommonsLinkedHashMap;
import com.helger.commons.collection.impl.CommonsLinkedHashSet;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.collection.impl.ICommonsOrderedMap;
import com.helger.commons.collection.impl.ICommonsOrderedSet;
import com.helger.commons.collection.impl.ICommonsSet;
import com.helger.commons.concurrent.SimpleReadWriteLock;
import com.helger.commons.debug.GlobalDebug;
import com.helger.commons.log.ConditionalLogger;
import com.helger.commons.log.IHasConditionalLogger;
import com.helger.commons.string.StringHelper;
/**
* This is a global cache for Locale objects to avoid too many object flowing
* around.
* This cache is application independent.
*
* @author Philip Helger
*/
@ThreadSafe
@Singleton
public class LocaleCache implements IHasConditionalLogger
{
/**
* Internal interface for a callback handler to be invoked, if a non-existing
* locale is found.
*
* @author Philip Helger
* @since 9.3.9
*/
@FunctionalInterface
public interface IMissingLocaleHandler
{
/**
* Called if a Locale is not yet present.
*
* @param sLocaleKey
* The key for the internal map. Never null
.
* @param sLanguage
* Language to use. Never null
.
* @param sCountry
* Country to use. Never null
.
* @param sVariant
* variant to use. Never null
.
* @return The created Locale or null
.
*/
@Nullable
Locale onMissingLocale (@Nonnull String sLocaleKey,
@Nonnull String sLanguage,
@Nonnull String sCountry,
@Nonnull String sVariant);
}
private static final class SingletonHolder
{
private static final LocaleCache INSTANCE = new LocaleCache ();
}
private static final Logger LOGGER = LoggerFactory.getLogger (LocaleCache.class);
private static final ConditionalLogger CONDLOG = new ConditionalLogger (LOGGER, !GlobalDebug.DEFAULT_SILENT_MODE);
private static boolean s_bDefaultInstantiated = false;
private final SimpleReadWriteLock m_aRWLock = new SimpleReadWriteLock ();
/** maps a string to a locale. */
@GuardedBy ("m_aRWLock")
private final ICommonsOrderedMap m_aLocales = new CommonsLinkedHashMap <> ();
private final IMissingLocaleHandler m_aMissingLocaleHandlerInsert = (sLocaleKey, l, c, v) -> {
// Insert in write lock
if (StringHelper.hasNoText (sLocaleKey))
return null;
return m_aRWLock.writeLockedGet ( () -> m_aLocales.computeIfAbsent (sLocaleKey, k -> new Locale (l, c, v)));
};
protected LocaleCache ()
{
reinitialize ();
}
/**
* @return true
if logging is disabled, false
if it
* is enabled.
* @since 9.4.0
*/
public static boolean isSilentMode ()
{
return CONDLOG.isDisabled ();
}
/**
* Enable or disable certain regular log messages.
*
* @param bSilentMode
* true
to disable logging, false
to enable
* logging
* @return The previous value of the silent mode.
* @since 9.4.0
*/
public static boolean setSilentMode (final boolean bSilentMode)
{
return !CONDLOG.setEnabled (!bSilentMode);
}
public static boolean isInstantiated ()
{
return s_bDefaultInstantiated;
}
@Nonnull
public static LocaleCache getInstance ()
{
final LocaleCache ret = SingletonHolder.INSTANCE;
s_bDefaultInstantiated = true;
return ret;
}
/**
* @return The {@link IMissingLocaleHandler} implementation of this instance
* that adds a missing locale to the set. Never null
.
* @since 9.4.2
*/
@Nonnull
public final IMissingLocaleHandler getDefaultMissingLocaleHandler ()
{
return m_aMissingLocaleHandlerInsert;
}
/**
* Get the {@link Locale} object matching the given language.
*
* @param sLanguage
* The language to use. May be null
or empty.
* @return null
if the passed language string is
* null
or empty
*/
@Nullable
public Locale getLocale (@Nullable final String sLanguage)
{
return getLocaleExt (sLanguage, m_aMissingLocaleHandlerInsert);
}
/**
* Get the {@link Locale} object matching the given language.
*
* @param sLanguage
* The language to use. May be null
or empty.
* @param aMissingHandler
* An optional handler to be invoked if the provided locale is not yet
* contained. May be null
.
* @return null
if the passed language string is
* null
or empty
* @since 9.3.9
*/
@Nullable
public Locale getLocaleExt (@Nullable final String sLanguage, @Nullable final IMissingLocaleHandler aMissingHandler)
{
if (sLanguage != null && sLanguage.length () > 2)
{
// parse
final String [] aParts = StringHelper.getExplodedArray (LocaleHelper.LOCALE_SEPARATOR, sLanguage, 3);
if (aParts.length == 3)
return getLocale (aParts[0], aParts[1], aParts[2], aMissingHandler);
if (aParts.length == 2)
return getLocale (aParts[0], aParts[1], "", aMissingHandler);
// else fall through
}
return getLocale (sLanguage, "", "", aMissingHandler);
}
/**
* Get the {@link Locale} object matching the given language and country.
*
* @param sLanguage
* The language to use. May be null
or empty.
* @param sCountry
* The country to use. May be null
.
* @return null
if the passed language string is
* null
or empty
*/
@Nullable
public Locale getLocale (@Nullable final String sLanguage, @Nullable final String sCountry)
{
return getLocale (sLanguage, sCountry, "");
}
/**
* Get the {@link Locale} object matching the given locale string
*
* @param sLanguage
* The language to use. May be null
or empty.
* @param sCountry
* Optional country to use. May be null
.
* @param sVariant
* Optional variant. May be null
.
* @return null
if all the passed parameters are
* null
or empty
*/
@Nullable
public Locale getLocale (@Nullable final String sLanguage,
@Nullable final String sCountry,
@Nullable final String sVariant)
{
// Try fetching again in writeLock
// not yet in cache, create a new one
// -> may lead to illegal locales, but simpler than the error handling
// for all the possible illegal values
return getLocale (sLanguage, sCountry, sVariant, m_aMissingLocaleHandlerInsert);
}
/**
* Build the locale key internally used. Note: this is not the same string as
* returned by {@link Locale#toString()}!!
*
* @param sLanguage
* Language to use
* @param sCountry
* Country to use
* @param sVariant
* Variant to use
* @return String
*/
@Nonnull
private static String _buildLocaleString (@Nonnull final String sLanguage,
@Nonnull final String sCountry,
@Nonnull final String sVariant)
{
final StringBuilder aLocaleSB = new StringBuilder ();
if (sLanguage.length () > 0)
aLocaleSB.append (sLanguage);
if (sCountry.length () > 0)
aLocaleSB.append (LocaleHelper.LOCALE_SEPARATOR).append (sCountry);
if (sVariant.length () > 0)
aLocaleSB.append (LocaleHelper.LOCALE_SEPARATOR).append (sVariant);
return aLocaleSB.toString ();
}
/**
* Get the {@link Locale} object matching the given locale string
*
* @param sLanguage
* The language to use. May be null
or empty.
* @param sCountry
* Optional country to use. May be null
.
* @param sVariant
* Optional variant. May be null
.
* @param aMissingHandler
* An optional handler to be invoked if the provided locale is not yet
* contained. May be null
.
* @return null
if all the passed parameters are
* null
or empty
* @since 9.3.9
*/
@Nullable
public Locale getLocale (@Nullable final String sLanguage,
@Nullable final String sCountry,
@Nullable final String sVariant,
@Nullable final IMissingLocaleHandler aMissingHandler)
{
final String sRealLanguage = StringHelper.getNotNull (LocaleHelper.getValidLanguageCode (sLanguage));
final String sRealCountry = StringHelper.getNotNull (LocaleHelper.getValidCountryCode (sCountry));
final String sRealVariant = StringHelper.getNotNull (sVariant);
final String sLocaleKey = _buildLocaleString (sRealLanguage, sRealCountry, sRealVariant);
Locale aLocale = null;
if (sLocaleKey.length () > 0)
{
// try to resolve locale
aLocale = m_aRWLock.readLockedGet ( () -> m_aLocales.get (sLocaleKey));
}
if (aLocale == null && aMissingHandler != null)
aLocale = aMissingHandler.onMissingLocale (sLocaleKey, sRealLanguage, sRealCountry, sRealVariant);
return aLocale;
}
/**
* Get all contained locales except the locales "all" and "independent"
*
* @return a set with all contained locales, except "all" and "independent".
* Never null
.
*/
@Nonnull
@ReturnsMutableCopy
public ICommonsList getAllLocales ()
{
final ICommonsList ret = m_aRWLock.readLockedGet (m_aLocales::copyOfValues);
ret.remove (LocaleHelper.LOCALE_ALL);
ret.remove (LocaleHelper.LOCALE_INDEPENDENT);
return ret;
}
/**
* Get all contained locales that consist only of a non-empty language.
*
* @return a set with all contained languages, except "all" and "independent"
*/
@Nonnull
@ReturnsMutableCopy
public ICommonsSet getAllLanguages ()
{
final ICommonsSet ret = new CommonsHashSet <> ();
for (final Locale aLocale : getAllLocales ())
{
final String sLanguage = aLocale.getLanguage ();
if (StringHelper.hasText (sLanguage))
ret.add (getLocale (sLanguage, null, null));
}
return ret;
}
/**
* Check if the passed language is in the cache.
*
* @param sLanguage
* The language to check.
* @return true
if it is in the cache, false
* otherwise.
*/
public boolean containsLocale (@Nullable final String sLanguage)
{
if (sLanguage != null && sLanguage.length () > 2)
{
// parse
final String [] aParts = StringHelper.getExplodedArray (LocaleHelper.LOCALE_SEPARATOR, sLanguage, 3);
if (aParts.length == 3)
return containsLocale (aParts[0], aParts[1], aParts[2]);
if (aParts.length == 2)
return containsLocale (aParts[0], aParts[1], "");
// else fall through
}
return containsLocale (sLanguage, "", "");
}
/**
* Check if the passed language is in the cache.
*
* @param sLanguage
* The language to check.
* @param sCountry
* The country to check.
* @return true
if it is in the cache, false
* otherwise.
*/
public boolean containsLocale (@Nullable final String sLanguage, @Nullable final String sCountry)
{
return containsLocale (sLanguage, sCountry, "");
}
@Nonnull
private static String _createLocaleKey (@Nullable final String sLanguage,
@Nullable final String sCountry,
@Nullable final String sVariant)
{
final String sRealLanguage = StringHelper.getNotNull (LocaleHelper.getValidLanguageCode (sLanguage));
final String sRealCountry = StringHelper.getNotNull (LocaleHelper.getValidCountryCode (sCountry));
final String sRealVariant = StringHelper.getNotNull (sVariant);
return _buildLocaleString (sRealLanguage, sRealCountry, sRealVariant);
}
/**
* Check if the passed language is in the cache.
*
* @param sLanguage
* The language to check.
* @param sCountry
* The country to check.
* @param sVariant
* The variant to check.
* @return true
if it is in the cache, false
* otherwise.
*/
public boolean containsLocale (@Nullable final String sLanguage,
@Nullable final String sCountry,
@Nullable final String sVariant)
{
final String sLocaleKey = _createLocaleKey (sLanguage, sCountry, sVariant);
if (sLocaleKey.length () == 0)
return false;
return m_aRWLock.readLockedBoolean ( () -> m_aLocales.containsKey (sLocaleKey));
}
/**
* @return A set of all system default locales. Never null
.
*/
@Nonnull
@ReturnsMutableCopy
public static ICommonsOrderedSet getAllDefaultLocales ()
{
final ICommonsOrderedSet ret = new CommonsLinkedHashSet <> (1024);
// add pseudo locales
ret.add (LocaleHelper.LOCALE_ALL);
ret.add (LocaleHelper.LOCALE_INDEPENDENT);
// add all predefined languages
for (final Locale aLocale : Locale.getAvailableLocales ())
{
ret.add (aLocale);
final String sCountry = aLocale.getCountry ();
final String sLanguage = aLocale.getLanguage ();
if (StringHelper.hasText (sCountry) && StringHelper.hasText (sLanguage))
{
// Add as country-only and as language-only locales as well
ret.add (new Locale ("", sCountry));
ret.add (new Locale (sLanguage, ""));
}
}
// http://forums.sun.com/thread.jspa?threadID=525482&tstart=1411
for (final String sCountry : Locale.getISOCountries ())
ret.add (new Locale ("", sCountry));
for (final String sLanguage : Locale.getISOLanguages ())
ret.add (new Locale (sLanguage, ""));
return ret;
}
/**
* Reset the cache to the initial state.
*/
public final void reinitialize ()
{
final ICommonsOrderedSet aDefLocales = getAllDefaultLocales ();
// Update map
m_aRWLock.writeLocked ( () -> {
m_aLocales.clear ();
for (final Locale aLocale : aDefLocales)
m_aLocales.put (aLocale.toString (), aLocale);
});
CONDLOG.debug ( () -> "Reinitialized " + LocaleCache.class.getName ());
}
}