LocalizableMessage.java
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions Copyrighted [year] [name of copyright owner]".
*
* Copyright 2009 Sun Microsystems, Inc.
* Portions copyright 2011-2012 ForgeRock AS
*/
package org.forgerock.i18n;
import java.io.Serializable;
import java.util.Formattable;
import java.util.Formatter;
import java.util.IllegalFormatException;
import java.util.Locale;
import org.forgerock.i18n.LocalizableMessageDescriptor.AbstractLocalizableMessageDescriptor;
/**
* A localizable message whose {@code String} representation can be retrieved in
* one or more locales. A message is localized each time it is converted to a
* {@code String} using one of its {@link #toString} methods.
* <p>
* Localizable messages are particularly useful in situations where a message a
* destined for multiple recipients, potentially in different locales. For
* example, a server application may record a message in its log file using its
* default locale, but also send the same message to the client using the
* client's locale (if known).
* <p>
* In most cases messages are intended for use in a locale-sensitive manner
* although this class defines convenience methods for creating non-localizable
* messages whose {@code String} representation is always the same regardless of
* the requested locale.
* <p>
* This class implements {@code CharSequence} so that messages can be supplied
* as arguments to other messages. This way messages can be composed of
* fragments of other messages if necessary.
*
* @see LocalizableMessageBuilder
*/
public final class LocalizableMessage implements CharSequence, Formattable,
Comparable<LocalizableMessage>, Serializable {
/**
* Generated serialization ID.
*/
private static final long serialVersionUID = 8011606572832995899L;
/**
* Represents an empty message string.
*/
public static final LocalizableMessage EMPTY = LocalizableMessage.raw("");
// Variable used to workaround a bug in AIX Java 1.6
// TODO: remove this code once the JDK issue referenced in 3077 is
// closed.
private static final boolean IS_AIX_POST5 = isAIXPost5();
/**
* Creates an non-localizable message whose {@code String} representation is
* always the same regardless of the requested locale.
* <p>
* Note that the types for {@code args} must be consistent with any argument
* specifiers appearing in {@code formatString} according to the rules of
* {@link java.util.Formatter}. A mismatch in type information will cause
* this message to render without argument substitution. Before using this
* method you should be sure that the message you are creating is not locale
* sensitive. If it is locale sensitive consider defining an appropriate
* {@link LocalizableMessageDescriptor}.
* <p>
* This method handles the special case where a {@code CharSequence} needs
* to be converted directly to {@code LocalizableMessage} as follows:
*
* <pre>
* String s = ...;
*
* // Both of these are equivalent:
* LocalizableMessage m = LocalizableMessage.raw(s);
* LocalizableMessage m = LocalizableMessage.raw("%s", s);
* </pre>
*
* @param formatString
* The raw message format string.
* @param args
* The raw message parameters.
* @return A non-localizable messages whose {@code String} representation is
* always the same regardless of the requested locale.
* @throws NullPointerException
* If {@code formatString} was {@code null}.
*/
public static LocalizableMessage raw(final CharSequence formatString,
final Object... args) {
if (formatString == null) {
throw new NullPointerException("formatString was null");
}
/*
* Experience with OpenDJ (see OPENDJ-142) has shown that this method is
* heavily abused in the single argument case where a developer wishes
* to convert a String to a LocalizableMessage:
*/
// String s = ...;
// LocalizableMessage m = LocalizableMessage.raw(s);
/*
* This will have unexpected behavior if the string contains a
* formatting character such as "%".
*/
if (args == null || args.length == 0) {
return LocalizableMessageDescriptor.RAW0.get(formatString);
} else {
return new LocalizableMessageDescriptor.Raw(formatString).get(args);
}
}
/**
* Creates a new message whose content is the {@code String} representation
* of the provided {@code Object}.
*
* @param object
* The object to be converted to a message, may be {@code null}.
* @return The new message.
*/
public static LocalizableMessage valueOf(final Object object) {
if (object instanceof LocalizableMessage) {
return (LocalizableMessage) object;
} else if (object instanceof LocalizableMessageBuilder) {
return ((LocalizableMessageBuilder) object).toMessage();
} else {
return raw(String.valueOf(object));
}
}
/**
* Returns whether we are running post 1.5 on AIX or not.
*
* @return {@code true} if we are running post 1.5 on AIX and {@code false}
* otherwise.
*/
private static boolean isAIXPost5() {
// TODO: remove this code once the JDK issue referenced in 3077 is
// closed.
boolean isJDK15 = false;
try {
final String javaRelease = System.getProperty("java.version");
isJDK15 = javaRelease.startsWith("1.5");
} catch (final Throwable t) {
System.err.println("Cannot get the java version: " + t);
}
final boolean isAIX = "aix".equalsIgnoreCase(System
.getProperty("os.name"));
return !isJDK15 && isAIX;
}
// Descriptor of this message.
private final AbstractLocalizableMessageDescriptor descriptor;
// Values used to replace argument specifiers in the format string.
private final Object[] args;
/**
* Creates a new parameterized message instance. See the class header for
* instructions on how to create messages outside of this package.
*
* @param descriptor
* The message descriptor.
* @param args
* The message parameters.
*/
LocalizableMessage(final AbstractLocalizableMessageDescriptor descriptor,
final Object... args) {
this.descriptor = descriptor;
this.args = args;
}
/**
* Returns the {@code char} value at the specified index of the
* {@code String} representation of this message in the default locale.
*
* @param index
* The index of the {@code char} value to be returned.
* @return The specified {@code char} value.
* @throws IndexOutOfBoundsException
* If the {@code index} argument is negative or not less than
* {@code length()}.
*/
public char charAt(final int index) {
return charAt(Locale.getDefault(), index);
}
/**
* Returns the {@code char} value at the specified index of the
* {@code String} representation of this message in the specified locale.
*
* @param locale
* The locale.
* @param index
* The index of the {@code char} value to be returned.
* @return The specified {@code char} value.
* @throws IndexOutOfBoundsException
* If the {@code index} argument is negative or not less than
* {@code length()}.
* @throws NullPointerException
* If {@code locale} was {@code null}.
*/
public char charAt(final Locale locale, final int index) {
return toString(locale).charAt(index);
}
/**
* Compares this message with the specified message for order in the default
* locale. Returns a negative integer, zero, or a positive integer as this
* object is less than, equal to, or greater than the specified object.
*
* @param message
* The message to be compared.
* @return A negative integer, zero, or a positive integer as this object is
* less than, equal to, or greater than the specified object.
*/
public int compareTo(final LocalizableMessage message) {
return toString().compareTo(message.toString());
}
/**
* Returns {@code true} if the provided object is a message whose
* {@code String} representation is equal to the {@code String}
* representation of this message in the default locale.
*
* @param o
* The object to be compared for equality with this message.
* @return {@code true} if this message is the equal to {@code o}, otherwise
* {@code false}.
*/
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
} else if (o instanceof LocalizableMessage) {
final LocalizableMessage message = (LocalizableMessage) o;
return toString().equals(message.toString());
} else {
return false;
}
}
/**
* Formats this message using the provided {@link Formatter}.
*
* @param formatter
* The {@link Formatter}.
* @param flags
* The flags modify the output format. The value is interpreted
* as a bitmask. Any combination of the following flags may be
* set: {@link java.util.FormattableFlags#LEFT_JUSTIFY},
* {@link java.util.FormattableFlags#UPPERCASE}, and
* {@link java.util.FormattableFlags#ALTERNATE}. If no flags are
* set, the default formatting of the implementing class will
* apply.
* @param width
* The minimum number of characters to be written to the output.
* If the length of the converted value is less than the
* {@code width} then the output will be padded by white space
* until the total number of characters equals width. The padding
* is at the beginning by default. If the
* {@link java.util.FormattableFlags#LEFT_JUSTIFY} flag is set
* then the padding will be at the end. If {@code width} is
* {@code -1} then there is no minimum.
* @param precision
* The maximum number of characters to be written to the output.
* The precision is applied before the width, thus the output
* will be truncated to {@code precision} characters even if the
* {@code width} is greater than the {@code precision}. If
* {@code precision} is {@code -1} then there is no explicit
* limit on the number of characters.
* @throws IllegalFormatException
* If any of the parameters are invalid. For specification of
* all possible formatting errors, see the <a
* href="../util/Formatter.html#detail">Details</a> section of
* the formatter class specification.
*/
public void formatTo(final Formatter formatter, final int flags,
final int width, final int precision) {
// Ignores flags, width and precision for now.
// see javadoc for Formattable
final Locale l = formatter.locale();
formatter.format(l, descriptor.getFormatString(l), args);
}
/**
* Returns the hash code value for this message calculated using the hash
* code of the {@code String} representation of this message in the default
* locale.
*
* @return The hash code value for this message.
*/
@Override
public int hashCode() {
return toString().hashCode();
}
/**
* Returns the length of the {@code String} representation of this message
* in the default locale.
*
* @return The length of the {@code String} representation of this message
* in the default locale.
*/
public int length() {
return length(Locale.getDefault());
}
/**
* Returns the length of the {@code String} representation of this message
* in the specified locale.
*
* @param locale
* The locale.
* @return The length of the {@code String} representation of this message
* in the specified locale.
* @throws NullPointerException
* If {@code locale} was {@code null}.
*/
public int length(final Locale locale) {
return toString(locale).length();
}
/**
* Returns the ordinal associated with this message, or {@code -1} if
* undefined. A message can be uniquely identified by its resource name and
* ordinal.
* <p>
* This may be useful when an application wishes to identify the source of a
* message. For example, a logging implementation could log the resource
* name in addition to the ordinal in order to unambiguously identify a
* message in a locale independent way.
*
* @return The ordinal associated with this descriptor, or {@code -1} if
* undefined.
* @see LocalizableMessage#resourceName()
*/
public int ordinal() {
return descriptor.ordinal();
}
/**
* Returns the name of the resource in which this message is defined. A
* message can be uniquely identified by its resource name and ordinal.
* <p>
* This may be useful when an application wishes to identify the source of a
* message. For example, a logging implementation could log the resource
* name in addition to the ordinal in order to unambiguously identify a
* message in a locale independent way.
* <p>
* The resource name may be used for obtaining named loggers, e.g. using
* SLF4J's {@code org.slf4j.LoggerFactory#getLogger(String name)}.
*
* @return The name of the resource in which this message is defined, or
* {@code null} if this message is a raw message and its source is
* undefined.
* @see LocalizableMessage#ordinal()
*/
public String resourceName() {
return descriptor.resourceName();
}
/**
* Returns a new {@code CharSequence} which is a subsequence of the
* {@code String} representation of this message in the default locale. The
* subsequence starts with the {@code char} value at the specified index and
* ends with the {@code char} value at index {@code end - 1} . The length
* (in {@code char}s) of the returned sequence is {@code end - start}, so if
* {@code start == end} then an empty sequence is returned.
*
* @param start
* The start index, inclusive.
* @param end
* The end index, exclusive.
* @return The specified subsequence.
* @throws IndexOutOfBoundsException
* If {@code start} or {@code end} are negative, if {@code end}
* is greater than {@code length()}, or if {@code start} is
* greater than {@code end}.
*/
public CharSequence subSequence(final int start, final int end) {
return subSequence(Locale.getDefault(), start, end);
}
/**
* Returns a new {@code CharSequence} which is a subsequence of the
* {@code String} representation of this message in the specified locale.
* The subsequence starts with the {@code char} value at the specified index
* and ends with the {@code char} value at index {@code end - 1} . The
* length (in {@code char}s) of the returned sequence is {@code end - start}
* , so if {@code start == end} then an empty sequence is returned.
*
* @param locale
* The locale.
* @param start
* The start index, inclusive.
* @param end
* The end index, exclusive.
* @return The specified subsequence.
* @throws IndexOutOfBoundsException
* If {@code start} or {@code end} are negative, if {@code end}
* is greater than {@code length()}, or if {@code start} is
* greater than {@code end}.
* @throws NullPointerException
* If {@code locale} was {@code null}.
*/
public CharSequence subSequence(final Locale locale, final int start,
final int end) {
return toString(locale).subSequence(start, end);
}
/**
* Returns the {@code String} representation of this message in the default
* locale.
*
* @return The {@code String} representation of this message.
*/
@Override
public String toString() {
return toString(Locale.getDefault());
}
/**
* Returns the {@code String} representation of this message in the
* specified locale.
*
* @param locale
* The locale.
* @return The {@code String} representation of this message.
* @throws NullPointerException
* If {@code locale} was {@code null}.
*/
@SuppressWarnings("resource")
public String toString(final Locale locale) {
String s;
final String fmt = descriptor.getFormatString(locale);
if (descriptor.requiresFormatter()) {
try {
// TODO: remove this code once the JDK issue referenced in 3077
// is closed.
if (IS_AIX_POST5) {
// Java 6 in AIX Formatter does not handle properly
// Formattable arguments; this code is a workaround for the
// problem.
boolean changeType = false;
for (final Object o : args) {
if (o instanceof Formattable) {
changeType = true;
break;
}
}
if (changeType) {
final Object[] newArgs = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Formattable) {
newArgs[i] = args[i].toString();
} else {
newArgs[i] = args[i];
}
}
s = new Formatter(locale).format(locale, fmt, newArgs)
.toString();
} else {
s = new Formatter(locale).format(locale, fmt, args)
.toString();
}
} else {
s = new Formatter(locale).format(locale, fmt, args)
.toString();
}
} catch (final IllegalFormatException e) {
// This should not happen with any of our internal messages.
// However, this may happen for raw messages that have a
// mismatch between argument specifier type and argument type.
s = fmt;
}
} else {
s = fmt;
}
if (s == null) {
s = "";
}
return s;
}
}