/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/urm/org/apache/slide/urm/utils/resourceutilities/ResourceLocator.java,v 1.4 2005/03/02 10:53:35 eckehard Exp $
 * $Revision: 1.4 $
 * $Date: 2005/03/02 10:53:35 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * 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 org.apache.slide.urm.utils.resourceutilities;

// import list

import java.net.URL;

import java.util.Locale;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.WeakHashMap;

//zsa//import com.softwareag.common.instrumentation.contract.Precondition;
//zsa//import com.softwareag.common.instrumentation.contract.Postcondition;


/**
 ** In contrast to the java.util.ResourceBundle.getBundle(), this class
 ** searches the classpath for resources with a given extension.
 **
	 ** @version $Revision: 1.4 $
 **
 ** @author rsk@softwareag.com
 **/
public class ResourceLocator {
	
	/**
	 ** Enables/disables precondition testing due to a global setting.
	 **/
	//zsa//private static final boolean PRE_CHECK = Precondition.isEnabled(ResourceLocator.class);
	
	/**
	 ** Enables/disables postcondition testing due to a global setting.
	 **/
	//zsa//private static final boolean POST_CHECK = Postcondition.isEnabled(ResourceLocator.class);
	
	
	/**
	 ** The CacheKey class bundles the name and the ClassLoader of a resource
	 ** and is used as a key for caching resources.
	 **
	 ** @version $Revision: 1.4 $
	 **
	 ** @author rsk@softwareag.com
	 **/
	protected static class CacheKey {
		
		/**
		 ** The name of the resource.
		 **/
		protected String resourceName = null;
		
		/**
		 ** The ClassLoader that located the URL associated with this key.
		 **/
		protected ClassLoader classLoader = null;
		
		
		/**
		 ** Constructs a CacheKey.
		 **
		 ** @param resourceName The name of the resource.
		 ** @param classLoader The ClassLoader that located the URL associated with this key.
		 **/
		public CacheKey(String resourceName, ClassLoader classLoader) {
			
			this.resourceName = resourceName;
			this.classLoader = classLoader;
		}
		
		/**
		 ** Returns <code>true</code>, if the other object is a CacheKey and
		 ** both the name and the ClassLoader of the resource are equal.
		 **
		 ** @pre        true
		 ** @post       true
		 **
		 ** @param      other  the Object to test for equality.
		 **
		 ** @return     <code>true</code>, if the other object is a CacheKey and
		 **             both the name and the ClassLoader of the resource are equal.
		 **/
		public boolean equals(Object other) {
			
			boolean isEqual = false;
			
			if (other instanceof CacheKey) {
				
				CacheKey otherCacheKey = (CacheKey)other;
				isEqual = resourceName.equals(otherCacheKey.resourceName);
				isEqual &= classLoader.equals(otherCacheKey.classLoader);
			}
			return isEqual;
		}
		
		/**
		 ** Overwrite hashCode() since {@link #equals equals()} is overwritten,
		 ** and equal object must have the same hash code.
		 **
		 ** @pre        true
		 ** @post       true
		 **
		 ** @return     the hash code of this instance.
		 **/
		public int hashCode() {
			
			return resourceName.hashCode() + 13*classLoader.hashCode();
		}
		
		/**
		 ** Returns a readable String representation of this instance.
		 **
		 ** @pre        true
		 ** @post       true
		 **
		 ** @return     a readable String representation of this instance.
		 **/
		public String toString() {
			
			StringBuffer buffer = new StringBuffer("CacheKey[");
			buffer.append(resourceName);
			buffer.append(", ");
			buffer.append(classLoader);
			buffer.append("]");
			
			return buffer.toString();
		}
	}
	
	/**
	 ** A container class that bundles the URL and the Locale of a resource.
	 **
	 ** @version $Revision: 1.4 $
	 **
	 ** @author rsk@softwareag.com
	 **/
	public static class ResourceLocation {
		
		/**
		 ** The URL of the resource.
		 **/
		URL url = null;
		
		/**
		 ** The Locale corresponding to the resource located at the URL.
		 **/
		Locale locale = null;
		
		/**
		 ** Creates a ResourceLocation.
		 **
		 ** @param url the URL of the resource.
		 ** @param locale the Locale corresponding to the resource located at the URL.
		 **/
		public ResourceLocation(URL url, Locale locale) {
			
			this.url = url;
			this.locale = locale;
		}
		
		/**
		 ** Returns the URL of the resource.
		 **
		 ** @return the URL of the resource.
		 **/
		public URL getURL() {
			
			return url;
		}
		
		/**
		 ** Returns the Locale corresponding to the resource located at the URL.
		 ** If the resource was located without any Locale-specific extension
		 ** (e.g. ..._de_DE), <code>null</code> is returned.
		 **
		 ** @return the Locale corresponding to the resource located at the URL.
		 **         If the resource was located without any Locale-specific extension
		 **         (e.g. ..._de_DE), <code>null</code> is returned.
		 **/
		public Locale getLocale() {
			
			return locale;
		}
		
		/**
		 ** Returns <code>true</code> if other is a ResourceLocation and the URLs
		 **         and Locales are equal.
		 **
		 ** @param    other  the Object to test for equality.
		 **
		 ** @return   <code>true</code> if other is a ResourceLocation and the URLs
		 **           and Locales are equal.
		 **/
		public boolean isEqual(Object other) {
			
			boolean isEqual = false;
			if (other instanceof ResourceLocation) {
				
				ResourceLocation otherLocation = (ResourceLocation)other;
				isEqual = getURL().equals(otherLocation.getURL());
				isEqual &= getLocale().equals(otherLocation.getLocale());
			}
			
			return isEqual;
		}
		
		/**
		 ** Overwrite hashCode(), since "equal" objects should have
		 ** the same hash code.
		 **
		 ** @return    the hash code of the instance.
		 **/
		public int hashCode() {
			
			return getURL().hashCode() + 13*getLocale().hashCode();
		}
	}
	
	
	/**
	 ** A container class that bundles the localized name (the name with the
	 ** locale specific extension, e.g. <code>resource_de_DE</code>) and the
	 ** Locale of a resource.
	 **
	 ** @version $Revision: 1.4 $
	 **
	 ** @author rsk@softwareag.com
	 **/
	public static class LocalizedResourceName {
		
		/**
		 ** The name of the resource.
		 **/
		String name = null;
		
		/**
		 ** The Locale of the resource.
		 **/
		Locale locale = null;
		
		/**
		 ** Creates a LocalizedResourceName.
		 **
		 ** @param name    the name of the resource.
		 ** @param locale  the Locale corresponding to the resource located at the URL.
		 **/
		public LocalizedResourceName(String name, Locale locale) {
			
			this.name = name;
			this.locale = locale;
		}
		
		/**
		 ** Returns the name of the resource.
		 **
		 ** @return the name of the resource.
		 **/
		public String getName() {
			return name;
		}
		
		/**
		 ** Returns the Locale of the resource.
		 **
		 ** @return the Locale of the resource.
		 **/
		public Locale getLocale() {
			return locale;
		}
		
		/**
		 ** Returns <code>true</code> if other is a ResourceLocation and the URLs
		 **         and Locales are equal.
		 **
		 ** @param    other  the Object to test for equality.
		 **
		 ** @return <code>true</code> if other is a ResourceLocation and the URLs
		 **         and Locales are equal.
		 **/
		public boolean isEqual(Object other) {
			
			boolean isEqual = false;
			if (other instanceof LocalizedResourceName) {
				
				LocalizedResourceName otherResource = (LocalizedResourceName)other;
				isEqual = getName().equals(otherResource.getName());
				isEqual &= getLocale().equals(otherResource.getLocale());
			}
			
			return isEqual;
		}
		
		/**
		 ** Overwrite hashCode(), since "equal" objects should have
		 ** the same hash code.
		 **
		 ** @return    the hash code of the instance.
		 **/
		public int hashCode() {
			return getName().hashCode() + 13*getLocale().hashCode();
		}
		
		/**
		 ** Returns a String representation of the instance.
		 **
		 ** @return     a String representation of the instance.
		 **/
		public String toString() {
			StringBuffer buffer = new StringBuffer("LocalizedResourceName[");
			buffer.append(getName());
			buffer.append(", ");
			buffer.append(getLocale());
			buffer.append("]");
			return buffer.toString();
		}
	}
	
	
	
	
	/**
	 ** A (WeakHash)Map storing the located resources' URL.
	 **/
	protected static Map cache = null;
	
	
	
	
	/**
	 ** Returns the best matching ResourceLocation found on the class path
	 ** for the given <code>resource</code> and <code>extension</code>.
	 ** If none was found, <code>null</code> is returned.
	 ** This is a convenience method that returns the first ResourceLocation of
	 ** the list returned by {@link #getAllResourceLocations getAllResourceLocations()}.
	 **
	 ** @param resource     the (full qualified) name of the resource,
	 **                     e.g. com.softwareag.exampleTemplate.
	 ** @param extension    the file extension, e.g. "xml".
	 **
	 ** @return             the best matching ResourceLocation found on the class path.
	 **                     If none was found, <code>null</code> is returned.
	 **/
	public static ResourceLocation getResourceLocation(String resource, String extension) {
		
		return getResourceLocation(resource, extension, null, null);
	}
	
	/**
	 ** Returns the best matching ResourceLocation found on the class path
	 ** for the given <code>resource</code>, <code>extension</code> and
	 ** <code>classLoader</code>.  If none was found, <code>null</code> is returned.
	 ** This is a convenience method that returns the first ResourceLocation of
	 ** the list returned by {@link #getAllResourceLocations getAllResourceLocations()}.
	 **
	 ** @param resource     the (full qualified) name of the resource,
	 **                     e.g. com.softwareag.exampleTemplate.
	 ** @param extension    the file extension, e.g. "xml".
	 ** @param classLoader  the ClassLoader used to locate the class.
	 **
	 ** @return             the best matching ResourceLocation found on the class path.
	 **                     If none was found, <code>null</code> is returned.
	 **/
	public static ResourceLocation getResourceLocation(String resource, String extension, ClassLoader classLoader) {
		
		return getResourceLocation(resource, extension, null, classLoader);
	}
	
	/**
	 ** Returns the best matching ResourceLocation found on the class path
	 ** for the given <code>resource</code>, <code>extension</code> and
	 ** <code>locale</code>.  If none was found, <code>null</code> is returned.
	 ** This is a convenience method that returns the first ResourceLocation of
	 ** the list returned by {@link #getAllResourceLocations getAllResourceLocations()}.
	 **
	 ** @param resource     the (full qualified) name of the resource,
	 **                     e.g. com.softwareag.exampleTemplate.
	 ** @param extension    the file extension, e.g. "xml".
	 ** @param locale       the preferred Locale.
	 **
	 ** @return             the best matching ResourceLocation found on the class path.
	 **                     If none was found, <code>null</code> is returned.
	 **/
	public static ResourceLocation getResourceLocation(String resource, String extension, Locale locale) {
		
		return getResourceLocation(resource, extension, locale, null);
	}
	
	/**
	 ** Returns the best matching ResourceLocation found on the class path
	 ** for the given <code>resource</code>, <code>extension</code>, <code>locale</code>
	 ** <code>classLoader</code>.  If none was found, <code>null</code> is returned.
	 ** This is a convenience method that returns the first ResourceLocation of
	 ** the list returned by {@link #getAllResourceLocations getAllResourceLocations()}.
	 **
	 ** @param resource     the (full qualified) name of the resource,
	 **                     e.g. com.softwareag.exampleTemplate.
	 ** @param extension    the file extension, e.g. "xml".
	 ** @param locale       the preferred Locale.
	 ** @param classLoader  the ClassLoader used to locate the class.
	 **
	 ** @return             the best matching ResourceLocation found on the class path.
	 **                     If none was found, <code>null</code> is returned.
	 **/
	public static ResourceLocation getResourceLocation(String resource, String extension, Locale locale, ClassLoader classLoader) {
		
		ResourceLocation resourceLocation = null;
		List resourceLocationList = getAllResourceLocations(resource, extension, locale, classLoader);
		if (resourceLocationList.size() > 0) {
			resourceLocation = (ResourceLocation)resourceLocationList.get(0);
		}
		
		return resourceLocation;
	}
	
	
	
	/**
	 ** Calls {@link #getLocalizedResourceNames getLocalizedResourceNames()}
	 ** with the given <code>resource</code> and <code>extension</code>
	 ** to get all matching names, and returns a list of all ResourceLocations
	 ** found on the class path. The list is sorted in descending order, means
	 ** from best matching to least matching.
	 ** (Mimics the mechanism of java.util.ResourceBundle).
	 **
	 ** @param resource     the (full qualified) name of the resource,
	 **                     e.g. com.softwareag.exampleTemplate.
	 ** @param extension    the file extension, e.g. "xml".
	 **
	 ** @return             a list of all ResourceLocations found on the class path.
	 **
	 ** @see java.util.ResourceBundle
	 **/
	public static List getAllResourceLocations(String resource, String extension) {
		
		return getAllResourceLocations(resource, extension, null, null);
	}
	
	/**
	 ** Calls {@link #getLocalizedResourceNames getLocalizedResourceNames()} with
	 ** the given <code>resource</code>, <code>extension</code> and <code>classLoader</code>
	 ** to get all matching names, and returns a list of all ResourceLocations
	 ** found on the class path. The list is sorted in descending order, means
	 ** from best matching to least matching.
	 ** (Mimics the mechanism of java.util.ResourceBundle).
	 **
	 ** @param resource     the (full qualified) name of the resource,
	 **                     e.g. com.softwareag.exampleTemplate.
	 ** @param extension    the file extension, e.g. "xml".
	 ** @param classLoader  the ClassLoader used to locate the class.
	 **
	 ** @return             a list of all ResourceLocations found on the class path.
	 **
	 ** @see java.util.ResourceBundle
	 **/
	public static List getAllResourceLocations(String resource, String extension, ClassLoader classLoader) {
		
		return getAllResourceLocations(resource, extension, null, classLoader);
	}
	
	/**
	 ** Calls {@link #getLocalizedResourceNames getLocalizedResourceNames()} with
	 ** the given <code>resource</code>, <code>extension</code> and <code>locale</code>
	 ** to get all matching names, and returns a list of all ResourceLocations
	 ** found on the class path. The list is sorted in descending order, means
	 ** from best matching to least matching.
	 ** (Mimics the mechanism of java.util.ResourceBundle).
	 **
	 ** @param resource     the (full qualified) name of the resource,
	 **                     e.g. com.softwareag.exampleTemplate.
	 ** @param extension    the file extension, e.g. "xml".
	 ** @param locale       the preferred Locale.
	 **
	 ** @return             a list of all ResourceLocations found on the class path.
	 **
	 ** @see java.util.ResourceBundle
	 **/
	public static List getAllResourceLocations(String resource, String extension, Locale locale) {
		
		return getAllResourceLocations(resource, extension, locale, null);
	}
	
	/**
	 ** Calls {@link #getLocalizedResourceNames getLocalizedResourceNames()} with
	 ** the given <code>resource</code>, <code>extension</code>, <code>locale</code>
	 ** and <code>classLoader</code>
	 ** to get all matching names, and returns a list of all ResourceLocations
	 ** found on the class path. The list is sorted in descending order, means
	 ** from best matching to least matching.
	 ** (Mimics the mechanism of java.util.ResourceBundle).
	 **
	 ** @param resource     the (full qualified) name of the resource,
	 **                     e.g. com.softwareag.exampleTemplate.
	 ** @param extension    the file extension, e.g. "xml".
	 ** @param locale       the preferred Locale.
	 ** @param classLoader  the ClassLoader used to locate the class.
	 **
	 ** @return             a list of all ResourceLocations found on the class path.
	 **
	 ** @see java.util.ResourceBundle
	 **/
	public static synchronized List getAllResourceLocations(String resource, String extension, Locale locale, ClassLoader classLoader) {
		
		List resourceLocationList = new ArrayList();
		List localizedResourceNameList = getLocalizedResourceNames(resource, extension, locale);
		
		if (classLoader == null) {
			classLoader = ResourceLocator.class.getClassLoader();
		}
		
		Iterator iterator = localizedResourceNameList.iterator();
		LocalizedResourceName localizedResourceName = null;
		CacheKey cacheKey = null;
		ResourceLocation resourceLocation = null;
		
		while (iterator.hasNext()) {
			
			localizedResourceName = (LocalizedResourceName)iterator.next();
			
			// WeakHashMap looses its references to fast!
			// Looking for a "SoftHashMap".
			cacheKey = new CacheKey(localizedResourceName.getName(), classLoader);
			if (cache != null) {
				resourceLocation = (ResourceLocation)cache.get(cacheKey);
			}
			if (resourceLocation == null) {
				URL url = classLoader.getResource(localizedResourceName.getName());
				
				if (url != null) {
					
					if (cache == null) {
						cache = new WeakHashMap();
					}
					resourceLocation = new ResourceLocation(url, localizedResourceName.getLocale());
					cache.put(cacheKey, resourceLocation);
				}
			}
			
			if (resourceLocation != null) {
				resourceLocationList.add(resourceLocation);
			}
		}
		
		return resourceLocationList;
	}
	
	
	/**
	 ** Creates a list of names from the given parameters
	 ** due to the rules specified in java.util.ResourceBundle.
	 **
	 ** @param resource the (full qualified) name of the resource,
	 **                 e.g. com.softwareag.exampleTemplate.
	 ** @param extension the file extension, e.g. "xml".
	 ** @param locale the preferred Locale.
	 **
	 ** @return a List of LocalizedResourceNames.
	 **
	 ** @see java.util.ResourceBundle
	 **/
	static List getLocalizedResourceNames(String resource, String extension, Locale locale) {
		
		List localizedResourceNameList = new ArrayList();
		
		if (resource != null) {
			
			if ( resource.startsWith("/") ) {
				
				resource = resource.substring(1);
			}
			resource = resource.replace('.', '/');
			
			getLocalizedResourceNames(resource, extension, locale, localizedResourceNameList);
			// add the entries for the default Locale only if it is different or weaker than
			// the given Locale, otherwise more specific locales than asked for might be returned
			if ( (locale == null) || checkForDifferentLanguageOrLessSpecific(locale, Locale.getDefault()) ) {
				getLocalizedResourceNames(resource, extension, Locale.getDefault(), localizedResourceNameList);
			}
			
			if (extension != null) {
				
				localizedResourceNameList.add(new LocalizedResourceName(resource + "." + extension, new Locale("", "")));
			}
			else {
				
				localizedResourceNameList.add(new LocalizedResourceName(resource, new Locale("", "")));
			}
		}
		
		return localizedResourceNameList;
	}
	
	
	/**
	 ** Creates a list of names from the given parameters.
	 **
	 ** @param resource               the (full qualified) name of the resource,
	 **                               e.g. com.softwareag.exampleTemplate.
	 ** @param extension              the file extension, e.g. "xml".
	 ** @param locale                 the Locale for which to create the names for.
	 ** @param localizedResourceNameList  the List to add the LocalizedResourceNames to.
	 **
	 ** @see java.util.ResourceBundle
	 **/
	protected static void getLocalizedResourceNames(String resource, String extension, Locale locale, List localizedResourceNameList) {
		
		if (locale != null) {
			
			StringBuffer buffer = new StringBuffer();
			
			if ( (locale.getLanguage().length() > 0) &
					(locale.getCountry().length() > 0) &
					(locale.getVariant().length() > 0) ) {
				
				buffer.setLength(0);
				buffer.append(resource);
				buffer.append("_");
				buffer.append(locale.getLanguage());
				buffer.append("_");
				buffer.append(locale.getCountry());
				buffer.append("_");
				buffer.append(locale.getVariant());
				if (extension != null) {
					
					buffer.append(".");
					buffer.append(extension);
				}
				localizedResourceNameList.add(new LocalizedResourceName(buffer.toString(),
																		new Locale(locale.getLanguage(), locale.getCountry(), locale.getVariant())));
			}
			
			if ( (locale.getLanguage().length() > 0) &
					(locale.getCountry().length() > 0) ) {
				
				buffer.setLength(0);
				buffer.append(resource);
				buffer.append("_");
				buffer.append(locale.getLanguage());
				buffer.append("_");
				buffer.append(locale.getCountry());
				if (extension != null) {
					
					buffer.append(".");
					buffer.append(extension);
				}
				localizedResourceNameList.add(new LocalizedResourceName(buffer.toString(),
																		new Locale(locale.getLanguage(), locale.getCountry())));
			}
			
			if ( (locale.getLanguage().length() > 0) ) {
				
				buffer.setLength(0);
				buffer.append(resource);
				buffer.append("_");
				buffer.append(locale.getLanguage());
				if (extension != null) {
					
					buffer.append(".");
					buffer.append(extension);
				}
				localizedResourceNameList.add(new LocalizedResourceName(buffer.toString(),
																		new Locale(locale.getLanguage(), "")));
			}
		}
	}
	
	/**
	 ** Returns <code>true</code>, if the Locale <code>toCheck</code> is either
	 ** a different language or otherwise less specific than the <code>original</code>:
	 **
	 ** <pre>
	 ** checkForDifferentLanguageOrLessSpecific(Locale.GERMAN, Locale.ENGLISH) == true
	 ** checkForDifferentLanguageOrLessSpecific(Locale.GERMANY, Locale.GERMAN) == true
	 ** checkForDifferentLanguageOrLessSpecific(Locale.GERMAN, Locale.GERMAN) == false
	 ** checkForDifferentLanguageOrLessSpecific(Locale.GERMAN, Locale.GERMANY) == false
	 ** </pre>
	 **
	 ** @pre        original != null
	 ** @pre        toCheck != null
	 ** @post       true
	 **
	 ** @param      original  the Locale to check the other to.
	 ** @param      toCheck   the Locale to check against the other.
	 **
	 ** @return     <code>true</code>, if the Locale <code>toCheck</code> is either
	 **             a different language or otherwise less specific than the
	 **             <code>original</code>
	 **/
	static boolean checkForDifferentLanguageOrLessSpecific(Locale original, Locale toCheck) {
		
		//zsa//if (PRE_CHECK) {Precondition.check("Parameter 'original' must not be null", original != null);}
		//zsa//if (PRE_CHECK) {Precondition.check("Parameter 'toCheck' must not be null", toCheck != null);}
		
		boolean differentOrLessSpecific = true;
		
		differentOrLessSpecific =
			(original.getLanguage().length() > 0) &&
			(! original.getLanguage().equals(toCheck.getLanguage()) );
		
		if (!differentOrLessSpecific) {
			differentOrLessSpecific =
				(original.getCountry().length() > 0) &&
				(toCheck.getCountry().length() == 0);
		}
		
		if (!differentOrLessSpecific) {
			differentOrLessSpecific =
				(original.getVariant().length() > 0) &&
				(toCheck.getVariant().length() == 0);
		}
		
		return differentOrLessSpecific;
	}
	
}
