/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library 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;
* version 2.1 of the License.
*
* This library 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.
*/
package org.geotools.resources;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.NoSuchElementException;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.opengis.util.InternationalString;
import org.geotools.util.logging.Logging;
/**
* {@link ResourceBundle} implementation using integers instead of strings for resource
* keys. Because it doesn't use strings, this implementation avoids adding all those string
* constants to {@code .class} files and runtime images. Developers still have meaningful labels
* in their code (e.g. {@code DIMENSION_MISMATCH}) through a set of constants defined in interfaces.
* This approach furthermore gives the benefit of compile-time safety. Because integer constants are
* inlined right into class files at compile time, the declarative interface is never loaded at run
* time. This class also provides facilities for string formatting using {@link MessageFormat}.
*
* @since 2.4
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class IndexedResourceBundle extends ResourceBundle {
/**
* Maximum string length for text inserted into another text. This parameter is used by
* {@link #summarize}. Resource strings are never cut to this length. However, text replacing
* "{0}" in a string like "Parameter name is {0}." will be cut to this length.
*/
private static final int MAX_STRING_LENGTH = 200;
/**
* The resource name of the binary file containing resources.
* It may be a file name or an entry in a JAR file.
*/
private final String filename;
/**
* The array of resources. Keys are an array index. For example, the value for key "14" is
* {@code values[14]}. This array will be loaded only when first needed. We should not load
* it at construction time, because some {@code ResourceBundle} objects will never ask for
* values. This is particularly the case for ancestor classes of {@code Resources_fr_CA},
* {@code Resources_en}, {@code Resources_de}, etc., which will only be used if a key has
* not been found in the subclass.
*/
private String[] values;
/**
* The locale for formatting objects like number, date, etc. There are two possible Locales
* we could use: default locale or resource bundle locale. If the default locale uses the same
* language as this ResourceBundle's locale, then we will use the default locale. This allows
* dates and numbers to be formatted according to user conventions (e.g. French Canada) even
* if the ResourceBundle locale is different (e.g. standard French). However, if languages
* don't match, then we will use ResourceBundle locale for better coherence.
*/
private transient Locale locale;
/**
* The object to use for formatting messages. This object
* will be constructed only when first needed.
*/
private transient MessageFormat format;
/**
* The key of the last resource requested. If the same resource is requested multiple times,
* knowing its key allows us to avoid invoking the costly {@link MessageFormat#applyPattern}
* method.
*/
private transient int lastKey;
/**
* Constructs a new resource bundle. The resource filename will be inferred from
* the fully qualified classname of this {@code IndexedResourceBundle} subclass.
*/
protected IndexedResourceBundle() {
filename = getClass().getSimpleName() + ".utf";
}
/**
* Constructs a new resource bundle.
*
* @param filename The resource name containing resources.
* It may be a filename or an entry in a JAR file.
*/
protected IndexedResourceBundle(final String filename) {
this.filename = filename;
}
/**
* Returns the locale to use for formatters.
*/
private Locale getFormatLocale() {
if (locale == null) {
locale = Locale.getDefault();
final Locale resourceLocale = getLocale();
if (!locale.getLanguage().equalsIgnoreCase(resourceLocale.getLanguage())) {
locale = resourceLocale;
}
}
return locale;
}
/**
* Returns the name of the package.
*/
private String getPackageName() {
final String name = getClass().getName();
final int index = name.lastIndexOf('.');
return (index>=0) ? name.substring(0, index) : "org.geotools";
}
/**
* Lists resources to the specified stream. If a resource has more than one line, only
* the first line will be written. This method is used mostly for debugging purposes.
*
* @param out The destination stream.
* @throws IOException if an output operation failed.
*/
public final void list(final Writer out) throws IOException {
// Synchronization performed by 'ensureLoaded'
list(out, ensureLoaded(null));
}
/**
* Lists resources to the specified stream. If a resource has more than one line, only
* the first line will be written. This method is used mostly for debugging purposes.
*
* @param out The destination stream.
* @param values The resources to list.
* @throws IOException if an output operation failed.
*/
private static void list(final Writer out, final String[] values) throws IOException {
final String lineSeparator = System.getProperty("line.separator", "\n");
for (int i=0; i<values.length; i++) {
String value = values[i];
if (value == null) {
continue;
}
int indexCR=value.indexOf('\r'); if (indexCR<0) indexCR=value.length();
int indexLF=value.indexOf('\n'); if (indexLF<0) indexLF=value.length();
final String number = String.valueOf(i);
out.write(Utilities.spaces(5-number.length()));
out.write(number);
out.write(":\t");
out.write(value.substring(0, Math.min(indexCR,indexLF)));
out.write(lineSeparator);
}
}
/**
* Ensures that resource values are loaded. If they are not, load them immediately.
*
* @param key Key for the requested resource, or {@code null} if all resources
* are requested. This key is used mostly for constructing messages.
* @return The resources.
* @throws MissingResourceException if this method failed to load resources.
*/
private String[] ensureLoaded(final String key) throws MissingResourceException {
LogRecord record = null;
try {
String[] values;
synchronized (this) {
values = this.values;
if (values != null) {
return values;
}
/*
* Prepares a log record. We will wait for successful loading before
* posting this record. If loading fails, the record will be changed
* into an error record. Note that the message must be logged outside
* the synchronized block, otherwise there is dead locks!
*/
record= new LogRecord(Level.FINER, "Loaded resources for {0} from bundle \"{1}\".");
record.setSourceClassName (getClass().getName());
record.setSourceMethodName((key != null) ? "getObject" : "getKeys");
/*
* Loads resources from the UTF file.
*/
InputStream in;
String name = filename;
while ((in = getClass().getResourceAsStream(name)) == null) {
final int ext = name.lastIndexOf('.');
final int lang = name.lastIndexOf('_', ext-1);
if (lang <= 0) {
throw new FileNotFoundException(filename);
}
name = name.substring(0, lang) + name.substring(ext);
}
final DataInputStream input = new DataInputStream(new BufferedInputStream(in));
this.values = values = new String[input.readInt()];
for (int i=0; i<values.length; i++) {
values[i] = input.readUTF();
if (values[i].length() == 0)
values[i] = null;
}
input.close();
/*
* Now, log the message. This message is not localized.
*/
String language = getLocale().getDisplayName(Locale.US);
if (language==null || language.length()==0) {
language="<default>";
}
record.setParameters(new String[]{language, getPackageName()});
}
final Logger logger = Logging.getLogger(IndexedResourceBundle.class);
record.setLoggerName(logger.getName());
logger.log(record);
return values;
} catch (IOException exception) {
record.setLevel (Level.WARNING);
record.setMessage(exception.getLocalizedMessage());
record.setThrown (exception);
final Logger logger = Logging.getLogger(IndexedResourceBundle.class);
record.setLoggerName(logger.getName());
logger.log(record);
final MissingResourceException error = new MissingResourceException(
exception.getLocalizedMessage(), getClass().getName(), key);
error.initCause(exception);
throw error;
}
}
/**
* Returns an enumeration of the keys.
*/
public final Enumeration<String> getKeys() {
// Synchronization performed by 'ensureLoaded'
final String[] values = ensureLoaded(null);
return new Enumeration<String>() {
private int i=0;
public boolean hasMoreElements() {
while (true) {
if (i >= values.length) return false;
if (values[i] != null) return true;
i++;
}
}
public String nextElement() {
while (true) {
if (i >= values.length) throw new NoSuchElementException();
if (values[i] != null) return String.valueOf(i++);
i++;
}
}
};
}
/**
* Gets an object for the given key from this resource bundle.
* Returns null if this resource bundle does not contain an
* object for the given key.
*
* @param key the key for the desired object
* @exception NullPointerException if {@code key} is {@code null}
* @return the object for the given key, or null
*/
protected final Object handleGetObject(final String key) {
// Synchronization performed by 'ensureLoaded'
final String[] values = ensureLoaded(key);
final int keyID;
try {
keyID = Integer.parseInt(key);
} catch (NumberFormatException exception) {
return null;
}
return (keyID>=0 && keyID<values.length) ? values[keyID] : null;
}
/**
* Makes sure that the {@code text} string is not longer than {@code maxLength} characters.
* If {@code text} is not longer, it is returned unchanged (except for trailing blanks,
* which are removed). If {@code text} is longer, it will be cut somewhere in the middle.
* This method tries to cut between two words and replace the missing words with "(...)".
* For example, the following string:
*
* <blockquote>
* "This sentence given as an example is way too long to be
* included in a message."
* </blockquote>
*
* May be "summarized" by something like this:
*
* <blockquote>
* "This sentence given (...) included in a message."
* </blockquote>
*
* @param text The sentence to summarize if it is too long.
* @param maxLength The maximum length allowed for {@code text}.
* If {@code text} is longer, it will be summarized.
* @return A sentence not longer than {@code maxLength}.
*/
private static String summarize(String text, int maxLength) {
text=text.trim();
final int length=text.length();
if (length<=maxLength) {
return text;
}
/*
* Computes maximum length for one half of the string. Take into
* account the space needed for inserting the " (...) " string.
*/
maxLength = (maxLength-7) >> 1;
if (maxLength<=0) {
return text;
}
/*
* We will remove characters from 'break1' to 'break2', both exclusive.
* We try to adjust 'break1' and 'break2' in such a way that the first
* and last characters to be removed will be spaces or punctuation
* characters.
* Constants 'lower' and 'upper' are limit values. If we don't find
* values for 'break1' and 'break2' inside those limits, we will give
* up.
*/
int break1 = maxLength;
int break2 = length-maxLength;
for (final int lower=(maxLength>>1); break1>=lower; break1--) {
if (!Character.isUnicodeIdentifierPart(text.charAt(break1))) {
while (--break1>=lower && !Character.isUnicodeIdentifierPart(text.charAt(break1)));
break;
}
}
for (final int upper=length-(maxLength>>1); break2<upper; break2++) {
if (!Character.isUnicodeIdentifierPart(text.charAt(break2))) {
while (++break2<upper && !Character.isUnicodeIdentifierPart(text.charAt(break2)));
break;
}
}
return (text.substring(0,break1+1)+" (...) "+text.substring(break2)).trim();
}
/**
* Returns {@code arguments} as an array. If {@code arguments} is already an array, this array
* or a copy of this array will be returned. If {@code arguments} is not an array, it will be
* placed in an array of length 1. In any case, all the array's elements will be checked for
* {@link String} objects. Any strings of length greater than {@link #MAX_STRING_LENGTH} will
* be reduced using the {@link #summarize} method.
*
* @param arguments The object to check.
* @return {@code arguments} as an array.
*/
private Object[] toArray(final Object arguments) {
Object[] array;
if (arguments instanceof Object[]) {
array = (Object[]) arguments;
} else {
array = new Object[] {arguments};
}
for (int i=0; i<array.length; i++) {
final Object element = array[i];
if (element instanceof CharSequence) {
final String s0;
if (element instanceof InternationalString) {
s0 = ((InternationalString) element).toString(getFormatLocale());
} else {
s0 = element.toString();
}
final String s1 = summarize(s0, MAX_STRING_LENGTH);
if (s0!=s1 && !s0.equals(s1)) {
if (array == arguments) {
array = new Object[array.length];
System.arraycopy(arguments, 0, array, 0, array.length);
}
array[i] = s1;
}
} else if (element instanceof Throwable) {
String message = ((Throwable) element).getLocalizedMessage();
if (message == null) {
message = Classes.getShortClassName(element);
}
array[i] = message;
} else if (element instanceof Class) {
array[i] = Classes.getShortName((Class<?>) element);
}
}
return array;
}
/**
* Gets a string for the given key and appends "..." to it.
* This method is typically used for creating menu items.
*
* @param key The key for the desired string.
* @return The string for the given key.
* @throws MissingResourceException If no object for the given key can be found.
*/
public final String getMenuLabel(final int key) throws MissingResourceException {
return getString(key) + "...";
}
/**
* Gets a string for the given key and appends ": " to it.
* This method is typically used for creating labels.
*
* @param key The key for the desired string.
* @return The string for the given key.
* @throws MissingResourceException If no object for the given key can be found.
*/
public final String getLabel(final int key) throws MissingResourceException {
return getString(key) + ": ";
}
/**
* Gets a string for the given key from this resource bundle or one of its parents.
*
* @param key The key for the desired string.
* @return The string for the given key.
* @throws MissingResourceException If no object for the given key can be found.
*/
public final String getString(final int key) throws MissingResourceException {
return getString(String.valueOf(key));
}
/**
* Gets a string for the given key and formats it with the specified argument. The message is
* formatted using {@link MessageFormat}. Calling this method is approximately equivalent to
* calling:
*
* <blockquote><pre>
* String pattern = getString(key);
* Format f = new MessageFormat(pattern);
* return f.format(arg0);
* </pre></blockquote>
*
* If {@code arg0} is not already an array, it will be placed into an array of length 1. Using
* {@link MessageFormat}, all occurrences of "{0}", "{1}", "{2}" in the resource string will be
* replaced by {@code arg0[0]}, {@code arg0[1]}, {@code arg0[2]}, etc.
*
* @param key The key for the desired string.
* @param arg0 A single object or an array of objects to be formatted and substituted.
* @return The string for the given key.
* @throws MissingResourceException If no object for the given key can be found.
*
* @see #getString(String)
* @see #getString(int,Object,Object)
* @see #getString(int,Object,Object,Object)
* @see MessageFormat
*/
public final String getString(final int key, final Object arg0) throws MissingResourceException {
final String pattern = getString(key);
final Object[] arguments = toArray(arg0);
synchronized (this) {
if (format == null) {
/*
* Constructs a new MessageFormat for formatting the arguments.
*/
format = new MessageFormat(pattern, getFormatLocale());
} else if (key != lastKey) {
/*
* Method MessageFormat.applyPattern(...) is costly! We will avoid
* calling it again if the format already has the right pattern.
*/
format.applyPattern(pattern);
lastKey = key;
}
return format.format(arguments);
}
}
/**
* Gets a string for the given key and replaces all occurrences of "{0}",
* "{1}", with values of {@code arg0}, {@code arg1}, etc.
*
* @param key The key for the desired string.
* @param arg0 Value to substitute for "{0}".
* @param arg1 Value to substitute for "{1}".
* @return The formatted string for the given key.
* @throws MissingResourceException If no object for the given key can be found.
*/
public final String getString(final int key,
final Object arg0,
final Object arg1) throws MissingResourceException
{
return getString(key, new Object[] {arg0, arg1});
}
/**
* Gets a string for the given key and replaces all occurrences of "{0}",
* "{1}", with values of {@code arg0}, {@code arg1}, etc.
*
* @param key The key for the desired string.
* @param arg0 Value to substitute for "{0}".
* @param arg1 Value to substitute for "{1}".
* @param arg2 Value to substitute for "{2}".
* @return The formatted string for the given key.
* @throws MissingResourceException If no object for the given key can be found.
*/
public final String getString(final int key,
final Object arg0,
final Object arg1,
final Object arg2) throws MissingResourceException
{
return getString(key, new Object[] {arg0, arg1, arg2});
}
/**
* Gets a string for the given key and replaces all occurrences of "{0}",
* "{1}", with values of {@code arg0}, {@code arg1}, etc.
*
* @param key The key for the desired string.
* @param arg0 Value to substitute for "{0}".
* @param arg1 Value to substitute for "{1}".
* @param arg2 Value to substitute for "{2}".
* @param arg3 Value to substitute for "{3}".
* @return The formatted string for the given key.
* @throws MissingResourceException If no object for the given key can be found.
*/
public final String getString(final int key,
final Object arg0,
final Object arg1,
final Object arg2,
final Object arg3) throws MissingResourceException
{
return getString(key, new Object[] {arg0, arg1, arg2, arg3});
}
/**
* Gets a string for the given key and replaces all occurrences of "{0}",
* "{1}", with values of {@code arg0}, {@code arg1}, etc.
*
* @param key The key for the desired string.
* @param arg0 Value to substitute for "{0}".
* @param arg1 Value to substitute for "{1}".
* @param arg2 Value to substitute for "{2}".
* @param arg3 Value to substitute for "{3}".
* @param arg4 Value to substitute for "{4}".
* @return The formatted string for the given key.
* @throws MissingResourceException If no object for the given key can be found.
*/
public final String getString(final int key,
final Object arg0,
final Object arg1,
final Object arg2,
final Object arg3,
final Object arg4) throws MissingResourceException
{
return getString(key, new Object[] {arg0, arg1, arg2, arg3, arg4});
}
/**
* Gets a localized log record.
*
* @param level The log record level.
* @param key The resource key.
* @return The log record.
*/
public LogRecord getLogRecord(final Level level, final int key) {
return getLogRecord(level, key, null);
}
/**
* Gets a localized log record.
*
* @param level The log record level.
* @param key The resource key.
* @param arg0 The parameter for the log message, or {@code null}.
* @return The log record.
*/
public LogRecord getLogRecord(final Level level, final int key,
final Object arg0)
{
final LogRecord record = new LogRecord(level, String.valueOf(key));
record.setResourceBundle(this);
if (arg0 != null) {
record.setParameters(toArray(arg0));
}
return record;
}
/**
* Gets a localized log record.
*
* @param level The log record level.
* @param key The resource key.
* @param arg0 The first parameter.
* @param arg1 The second parameter.
* @return The log record.
*/
public LogRecord getLogRecord(final Level level, final int key,
final Object arg0,
final Object arg1)
{
return getLogRecord(level, key, new Object[]{arg0, arg1});
}
/**
* Gets a localized log record.
*
* @param level The log record level.
* @param key The resource key.
* @param arg0 The first parameter.
* @param arg1 The second parameter.
* @param arg2 The third parameter.
* @return The log record.
*/
public LogRecord getLogRecord(final Level level, final int key,
final Object arg0,
final Object arg1,
final Object arg2)
{
return getLogRecord(level, key, new Object[]{arg0, arg1, arg2});
}
/**
* Gets a localized log record.
*
* @param level The log record level.
* @param key The resource key.
* @param arg0 The first parameter.
* @param arg1 The second parameter.
* @param arg2 The third parameter.
* @param arg3 The fourth parameter.
* @return The log record.
*/
public LogRecord getLogRecord(final Level level, final int key,
final Object arg0,
final Object arg1,
final Object arg2,
final Object arg3)
{
return getLogRecord(level, key, new Object[]{arg0, arg1, arg2, arg3});
}
/**
* Localize and format the message string from a log record. This method performs a work
* similar to {@link java.util.logging.Formatter#formatMessage}, except that the work will be
* delegated to {@link #getString(int, Object)} if the {@linkplain LogRecord#getResourceBundle
* record resource bundle} is an instance of {@code IndexedResourceBundle}.
*
* @param record The log record to format.
* @return The formatted message.
*/
public static String format(final LogRecord record) {
String message = record.getMessage();
final ResourceBundle resources = record.getResourceBundle();
if (resources instanceof IndexedResourceBundle) {
int key = -1;
try {
key = Integer.parseInt(message);
} catch (NumberFormatException e) {
unexpectedException(e);
}
if (key >= 0) {
final Object[] parameters = record.getParameters();
return ((IndexedResourceBundle) resources).getString(key, parameters);
}
}
if (resources != null) {
try {
message = resources.getString(message);
} catch (MissingResourceException e) {
unexpectedException(e);
}
final Object[] parameters = record.getParameters();
if (parameters != null && parameters.length != 0) {
final int offset = message.indexOf('{');
if (offset>=0 && offset<message.length()-1) {
// Uses a more restrictive check than Character.isDigit(char)
final char c = message.charAt(offset);
if (c>='0' && c<='9') try {
return MessageFormat.format(message, parameters);
} catch (IllegalArgumentException e) {
unexpectedException(e);
}
}
}
}
return message;
}
/**
* Invoked when an unexpected exception occured in the {@link #format} method.
*/
private static void unexpectedException(final RuntimeException exception) {
Logging.unexpectedException(IndexedResourceBundle.class, "format", exception);
}
/**
* Returns a string representation of this object.
* This method is for debugging purposes only.
*/
@Override
public synchronized String toString() {
final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this));
buffer.append('[');
if (values != null) {
int count = 0;
for (int i=0; i<values.length; i++) {
if (values[i]!=null) count++;
}
buffer.append(count);
buffer.append(" values");
}
buffer.append(']');
return buffer.toString();
}
}