LocaleUtility.java

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.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://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.util;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.GlobalProperty;
import org.openmrs.api.GlobalPropertyListener;
import org.openmrs.api.context.Context;
import org.springframework.util.StringUtils;

/**
 * A utility class for working with Locales.
 */
public class LocaleUtility implements GlobalPropertyListener {
	
	private static Log log = LogFactory.getLog(LocaleUtility.class);
	
	/**
	 * Cached version of the default locale. This is cached so that we don't have to look it up in
	 * the global property table every page load
	 */
	private static Locale defaultLocaleCache = null;
	
	/**
	 * Cached version of the localeAllowedList. This is cached so that we don't have to look it up
	 * in the global property table every page load
	 */
	private static List<Locale> localesAllowedListCache = null;
	
	/**
	 * Default internal locale.
	 * 
	 * @deprecated use {@link #getDefaultLocale()} now
	 */
	@Deprecated
	public static final Locale DEFAULT_LOCALE = Locale.UK;
	
	/**
	 * Gets the default locale specified as a global property.
	 * 
	 * @return default locale object.
	 * @since 1.5
	 * @should not return null if global property does not exist
	 * @should not fail with empty global property value
	 * @should not fail with bogus global property value
	 * @should return locale object for global property
	 * @should not cache locale when session is not open
	 */
	public static Locale getDefaultLocale() {
		if (defaultLocaleCache == null) {
			if (Context.isSessionOpen()) {
				try {
					String locale = Context.getAdministrationService().getGlobalProperty(
					    OpenmrsConstants.GLOBAL_PROPERTY_DEFAULT_LOCALE);
					
					if (StringUtils.hasLength(locale)) {
						try {
							defaultLocaleCache = fromSpecification(locale);
						}
						catch (Exception t) {
							log.warn("Unable to parse default locale global property value: " + locale, t);
						}
					}
				}
				catch (Throwable t) {
					// swallow most of the stack trace for most users
					log.warn("Unable to get locale global property value. " + t.getMessage());
					log.trace("Unable to get locale global property value", t);
				}
				
				// if we weren't able to load the locale from the global property,
				// use the default one
				if (defaultLocaleCache == null)
					defaultLocaleCache = fromSpecification(OpenmrsConstants.GLOBAL_PROPERTY_DEFAULT_LOCALE_DEFAULT_VALUE);
			} else {
				// if session is not open, return the default locale without caching
				return fromSpecification(OpenmrsConstants.GLOBAL_PROPERTY_DEFAULT_LOCALE_DEFAULT_VALUE);
			}
			
		}
		
		return defaultLocaleCache;
	}
	
	/**
	 * Compatible is a looser matching than that provided by Locale.equals(). Two locales are
	 * considered equal if they are equal, or if either does not have a country specified and the
	 * languages match.
	 * 
	 * @param lhs left hand side Locale
	 * @param rhs right hand side Locale
	 * @return true if the two locales are compatible, false otherwise
	 * @should confirm different language missing country as compatible
	 * @should confirm same language missing country as compatible
	 * @should not confirm different country as compatible
	 * @should confirm matching country as compatible
	 * @should not confirm different language as compatible
	 * @should confirm matching language as compatible
	 */
	public static boolean areCompatible(Locale lhs, Locale rhs) {
		if (lhs.equals(rhs)) {
			return true;
		} else if (("".equals(lhs.getCountry())) || ("".equals(rhs.getCountry()))) {
			// no country specified, so language match is good enough
			if (lhs.getLanguage().equals(rhs.getLanguage())) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Creates a locale based on a string specification. The specification must be conform with the
	 * following format: ll_CC_vv <br/>
	 * <ul>
	 * <li>ll: two-character lowercase ISO-639 language code
	 * <li>CC: two-character uppercase ISO-3166 country code optional
	 * <li>vv: arbitrary length variant code
	 * </ul>
	 * For example: en_US_Traditional_WIN ...represents English language in the United States with
	 * the traditional collation for windows.
	 * 
	 * @param localeSpecification encoded locale specification
	 * @return the representative Locale, or null if the specification is invalid
	 * @should get locale from two character language code
	 * @should get locale from language code and country code
	 * @should get locale from language code country code and variant
	 */
	public static Locale fromSpecification(String localeSpecification) {
		Locale createdLocale = null;
		
		localeSpecification = localeSpecification.trim();
		
		String[] localeComponents = localeSpecification.split("_");
		if (localeComponents.length == 1) {
			createdLocale = new Locale(localeComponents[0]);
		} else if (localeComponents.length == 2) {
			createdLocale = new Locale(localeComponents[0], localeComponents[1]);
		} else if (localeComponents.length > 2) {
			String variant = localeSpecification.substring(localeSpecification.indexOf(localeComponents[2])); // gets everything after the
			// second underscore
			createdLocale = new Locale(localeComponents[0], localeComponents[1], variant);
		}
		
		return createdLocale;
	}
	
	/**
	 * Utility method that returns a collection of all openmrs system locales, the set includes the
	 * current logged in user's preferred locale if any is set, the default locale, allowed locales
	 * in the order they are specified in the 'allowed.locale.list' global property and 'en' at the
	 * very end of the set if it isn't yet among them.
	 * 
	 * @returns a collection of all specified and allowed locales with no duplicates.
	 * @should return a set of locales with a predictable order
	 * @should return a set of locales with no duplicates
	 * @should have default locale as the first element if user has no preferred locale
	 * @should have default locale as the second element if user has a preferred locale
	 * @should always have english included in the returned collection
	 * @should always have default locale default value included in the returned collection
	 * @since 1.7
	 */
	public static Set<Locale> getLocalesInOrder() {
		
		Set<Locale> locales = new LinkedHashSet<Locale>();
		locales.add(Context.getLocale());
		locales.add(getDefaultLocale());
		if (localesAllowedListCache == null)
			localesAllowedListCache = Context.getAdministrationService().getAllowedLocales();
		
		if (localesAllowedListCache != null)
			locales.addAll(localesAllowedListCache);
		
		locales.add(Locale.ENGLISH);
		locales.add(fromSpecification(OpenmrsConstants.GLOBAL_PROPERTY_DEFAULT_LOCALE_DEFAULT_VALUE));
		
		return locales;
	}
	
	@Override
	public void globalPropertyChanged(GlobalProperty newValue) {
		// reset the value
		defaultLocaleCache = null;
		localesAllowedListCache = null;
	}
	
	@Override
	public void globalPropertyDeleted(String propertyName) {
		// reset the value
		defaultLocaleCache = null;
		localesAllowedListCache = null;
	}
	
	@Override
	public boolean supportsPropertyName(String propertyName) {
		return propertyName.equals(OpenmrsConstants.GLOBAL_PROPERTY_DEFAULT_LOCALE)
		        || propertyName.equals(OpenmrsConstants.GLOBAL_PROPERTY_LOCALE_ALLOWED_LIST);
		
	}
	
	/**
	 * Checks if specified locale object is valid
	 * 
	 * @param locale
	 *            object for validation
	 * @return true if locale is available
	 */
	public static boolean isValid(Locale locale) {
		try {
			return locale.getISO3Language() != null && locale.getISO3Country() != null;
		}
		catch (MissingResourceException e) {
			return false;
		}
	}
	
}