001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyrighted [year] [name of copyright owner]".
013 *
014 *      Copyright 2009 Sun Microsystems, Inc.
015 *      Portions copyright 2011-2012 ForgeRock AS
016 */
017
018package org.forgerock.i18n;
019
020import java.io.Serializable;
021import java.util.Formattable;
022import java.util.Formatter;
023import java.util.IllegalFormatException;
024import java.util.Locale;
025
026import org.forgerock.i18n.LocalizableMessageDescriptor.AbstractLocalizableMessageDescriptor;
027
028/**
029 * A localizable message whose {@code String} representation can be retrieved in
030 * one or more locales. A message is localized each time it is converted to a
031 * {@code String} using one of its {@link #toString} methods.
032 * <p>
033 * Localizable messages are particularly useful in situations where a message a
034 * destined for multiple recipients, potentially in different locales. For
035 * example, a server application may record a message in its log file using its
036 * default locale, but also send the same message to the client using the
037 * client's locale (if known).
038 * <p>
039 * In most cases messages are intended for use in a locale-sensitive manner
040 * although this class defines convenience methods for creating non-localizable
041 * messages whose {@code String} representation is always the same regardless of
042 * the requested locale.
043 * <p>
044 * This class implements {@code CharSequence} so that messages can be supplied
045 * as arguments to other messages. This way messages can be composed of
046 * fragments of other messages if necessary.
047 *
048 * @see LocalizableMessageBuilder
049 */
050public final class LocalizableMessage implements CharSequence, Formattable,
051        Comparable<LocalizableMessage>, Serializable {
052
053    /**
054     * Generated serialization ID.
055     */
056    private static final long serialVersionUID = 8011606572832995899L;
057
058    /**
059     * Represents an empty message string.
060     */
061    public static final LocalizableMessage EMPTY = LocalizableMessage.raw("");
062
063    // Variable used to workaround a bug in AIX Java 1.6
064    // TODO: remove this code once the JDK issue referenced in 3077 is
065    // closed.
066    private static final boolean IS_AIX_POST5 = isAIXPost5();
067
068    /**
069     * Creates an non-localizable message whose {@code String} representation is
070     * always the same regardless of the requested locale.
071     * <p>
072     * Note that the types for {@code args} must be consistent with any argument
073     * specifiers appearing in {@code formatString} according to the rules of
074     * {@link java.util.Formatter}. A mismatch in type information will cause
075     * this message to render without argument substitution. Before using this
076     * method you should be sure that the message you are creating is not locale
077     * sensitive. If it is locale sensitive consider defining an appropriate
078     * {@link LocalizableMessageDescriptor}.
079     * <p>
080     * This method handles the special case where a {@code CharSequence} needs
081     * to be converted directly to {@code LocalizableMessage} as follows:
082     *
083     * <pre>
084     * String s = ...;
085     *
086     * // Both of these are equivalent:
087     * LocalizableMessage m = LocalizableMessage.raw(s);
088     * LocalizableMessage m = LocalizableMessage.raw("%s", s);
089     * </pre>
090     *
091     * @param formatString
092     *            The raw message format string.
093     * @param args
094     *            The raw message parameters.
095     * @return A non-localizable messages whose {@code String} representation is
096     *         always the same regardless of the requested locale.
097     * @throws NullPointerException
098     *             If {@code formatString} was {@code null}.
099     */
100    public static LocalizableMessage raw(final CharSequence formatString,
101            final Object... args) {
102        if (formatString == null) {
103            throw new NullPointerException("formatString was null");
104        }
105
106        /*
107         * Experience with OpenDJ (see OPENDJ-142) has shown that this method is
108         * heavily abused in the single argument case where a developer wishes
109         * to convert a String to a LocalizableMessage:
110         */
111        // String s = ...;
112        // LocalizableMessage m = LocalizableMessage.raw(s);
113
114        /*
115         * This will have unexpected behavior if the string contains a
116         * formatting character such as "%".
117         */
118        if (args == null || args.length == 0) {
119            return LocalizableMessageDescriptor.RAW0.get(formatString);
120        } else {
121            return new LocalizableMessageDescriptor.Raw(formatString).get(args);
122        }
123    }
124
125    /**
126     * Creates a new message whose content is the {@code String} representation
127     * of the provided {@code Object}.
128     *
129     * @param object
130     *            The object to be converted to a message, may be {@code null}.
131     * @return The new message.
132     */
133    public static LocalizableMessage valueOf(final Object object) {
134        if (object instanceof LocalizableMessage) {
135            return (LocalizableMessage) object;
136        } else if (object instanceof LocalizableMessageBuilder) {
137            return ((LocalizableMessageBuilder) object).toMessage();
138        } else {
139            return raw(String.valueOf(object));
140        }
141    }
142
143    /**
144     * Returns whether we are running post 1.5 on AIX or not.
145     *
146     * @return {@code true} if we are running post 1.5 on AIX and {@code false}
147     *         otherwise.
148     */
149    private static boolean isAIXPost5() {
150        // TODO: remove this code once the JDK issue referenced in 3077 is
151        // closed.
152        boolean isJDK15 = false;
153        try {
154            final String javaRelease = System.getProperty("java.version");
155            isJDK15 = javaRelease.startsWith("1.5");
156        } catch (final Throwable t) {
157            System.err.println("Cannot get the java version: " + t);
158        }
159        final boolean isAIX = "aix".equalsIgnoreCase(System
160                .getProperty("os.name"));
161        return !isJDK15 && isAIX;
162    }
163
164    // Descriptor of this message.
165    private final AbstractLocalizableMessageDescriptor descriptor;
166
167    // Values used to replace argument specifiers in the format string.
168    private final Object[] args;
169
170    /**
171     * Creates a new parameterized message instance. See the class header for
172     * instructions on how to create messages outside of this package.
173     *
174     * @param descriptor
175     *            The message descriptor.
176     * @param args
177     *            The message parameters.
178     */
179    LocalizableMessage(final AbstractLocalizableMessageDescriptor descriptor,
180            final Object... args) {
181        this.descriptor = descriptor;
182        this.args = args;
183    }
184
185    /**
186     * Returns the {@code char} value at the specified index of the
187     * {@code String} representation of this message in the default locale.
188     *
189     * @param index
190     *            The index of the {@code char} value to be returned.
191     * @return The specified {@code char} value.
192     * @throws IndexOutOfBoundsException
193     *             If the {@code index} argument is negative or not less than
194     *             {@code length()}.
195     */
196    public char charAt(final int index) {
197        return charAt(Locale.getDefault(), index);
198    }
199
200    /**
201     * Returns the {@code char} value at the specified index of the
202     * {@code String} representation of this message in the specified locale.
203     *
204     * @param locale
205     *            The locale.
206     * @param index
207     *            The index of the {@code char} value to be returned.
208     * @return The specified {@code char} value.
209     * @throws IndexOutOfBoundsException
210     *             If the {@code index} argument is negative or not less than
211     *             {@code length()}.
212     * @throws NullPointerException
213     *             If {@code locale} was {@code null}.
214     */
215    public char charAt(final Locale locale, final int index) {
216        return toString(locale).charAt(index);
217    }
218
219    /**
220     * Compares this message with the specified message for order in the default
221     * locale. Returns a negative integer, zero, or a positive integer as this
222     * object is less than, equal to, or greater than the specified object.
223     *
224     * @param message
225     *            The message to be compared.
226     * @return A negative integer, zero, or a positive integer as this object is
227     *         less than, equal to, or greater than the specified object.
228     */
229    public int compareTo(final LocalizableMessage message) {
230        return toString().compareTo(message.toString());
231    }
232
233    /**
234     * Returns {@code true} if the provided object is a message whose
235     * {@code String} representation is equal to the {@code String}
236     * representation of this message in the default locale.
237     *
238     * @param o
239     *            The object to be compared for equality with this message.
240     * @return {@code true} if this message is the equal to {@code o}, otherwise
241     *         {@code false}.
242     */
243    @Override
244    public boolean equals(final Object o) {
245        if (this == o) {
246            return true;
247        } else if (o instanceof LocalizableMessage) {
248            final LocalizableMessage message = (LocalizableMessage) o;
249            return toString().equals(message.toString());
250        } else {
251            return false;
252        }
253    }
254
255    /**
256     * Formats this message using the provided {@link Formatter}.
257     *
258     * @param formatter
259     *            The {@link Formatter}.
260     * @param flags
261     *            The flags modify the output format. The value is interpreted
262     *            as a bitmask. Any combination of the following flags may be
263     *            set: {@link java.util.FormattableFlags#LEFT_JUSTIFY},
264     *            {@link java.util.FormattableFlags#UPPERCASE}, and
265     *            {@link java.util.FormattableFlags#ALTERNATE}. If no flags are
266     *            set, the default formatting of the implementing class will
267     *            apply.
268     * @param width
269     *            The minimum number of characters to be written to the output.
270     *            If the length of the converted value is less than the
271     *            {@code width} then the output will be padded by white space
272     *            until the total number of characters equals width. The padding
273     *            is at the beginning by default. If the
274     *            {@link java.util.FormattableFlags#LEFT_JUSTIFY} flag is set
275     *            then the padding will be at the end. If {@code width} is
276     *            {@code -1} then there is no minimum.
277     * @param precision
278     *            The maximum number of characters to be written to the output.
279     *            The precision is applied before the width, thus the output
280     *            will be truncated to {@code precision} characters even if the
281     *            {@code width} is greater than the {@code precision}. If
282     *            {@code precision} is {@code -1} then there is no explicit
283     *            limit on the number of characters.
284     * @throws IllegalFormatException
285     *             If any of the parameters are invalid. For specification of
286     *             all possible formatting errors, see the <a
287     *             href="../util/Formatter.html#detail">Details</a> section of
288     *             the formatter class specification.
289     */
290    public void formatTo(final Formatter formatter, final int flags,
291            final int width, final int precision) {
292        // Ignores flags, width and precision for now.
293        // see javadoc for Formattable
294        final Locale l = formatter.locale();
295        formatter.format(l, descriptor.getFormatString(l), args);
296    }
297
298    /**
299     * Returns the hash code value for this message calculated using the hash
300     * code of the {@code String} representation of this message in the default
301     * locale.
302     *
303     * @return The hash code value for this message.
304     */
305    @Override
306    public int hashCode() {
307        return toString().hashCode();
308    }
309
310    /**
311     * Returns the length of the {@code String} representation of this message
312     * in the default locale.
313     *
314     * @return The length of the {@code String} representation of this message
315     *         in the default locale.
316     */
317    public int length() {
318        return length(Locale.getDefault());
319    }
320
321    /**
322     * Returns the length of the {@code String} representation of this message
323     * in the specified locale.
324     *
325     * @param locale
326     *            The locale.
327     * @return The length of the {@code String} representation of this message
328     *         in the specified locale.
329     * @throws NullPointerException
330     *             If {@code locale} was {@code null}.
331     */
332    public int length(final Locale locale) {
333        return toString(locale).length();
334    }
335
336    /**
337     * Returns the ordinal associated with this message, or {@code -1} if
338     * undefined. A message can be uniquely identified by its resource name and
339     * ordinal.
340     * <p>
341     * This may be useful when an application wishes to identify the source of a
342     * message. For example, a logging implementation could log the resource
343     * name in addition to the ordinal in order to unambiguously identify a
344     * message in a locale independent way.
345     *
346     * @return The ordinal associated with this descriptor, or {@code -1} if
347     *         undefined.
348     * @see LocalizableMessage#resourceName()
349     */
350    public int ordinal() {
351        return descriptor.ordinal();
352    }
353
354    /**
355     * Returns the name of the resource in which this message is defined. A
356     * message can be uniquely identified by its resource name and ordinal.
357     * <p>
358     * This may be useful when an application wishes to identify the source of a
359     * message. For example, a logging implementation could log the resource
360     * name in addition to the ordinal in order to unambiguously identify a
361     * message in a locale independent way.
362     * <p>
363     * The resource name may be used for obtaining named loggers, e.g. using
364     * SLF4J's {@code org.slf4j.LoggerFactory#getLogger(String name)}.
365     *
366     * @return The name of the resource in which this message is defined, or
367     *         {@code null} if this message is a raw message and its source is
368     *         undefined.
369     * @see LocalizableMessage#ordinal()
370     */
371    public String resourceName() {
372        return descriptor.resourceName();
373    }
374
375    /**
376     * Returns a new {@code CharSequence} which is a subsequence of the
377     * {@code String} representation of this message in the default locale. The
378     * subsequence starts with the {@code char} value at the specified index and
379     * ends with the {@code char} value at index {@code end - 1} . The length
380     * (in {@code char}s) of the returned sequence is {@code end - start}, so if
381     * {@code start == end} then an empty sequence is returned.
382     *
383     * @param start
384     *            The start index, inclusive.
385     * @param end
386     *            The end index, exclusive.
387     * @return The specified subsequence.
388     * @throws IndexOutOfBoundsException
389     *             If {@code start} or {@code end} are negative, if {@code end}
390     *             is greater than {@code length()}, or if {@code start} is
391     *             greater than {@code end}.
392     */
393    public CharSequence subSequence(final int start, final int end) {
394        return subSequence(Locale.getDefault(), start, end);
395    }
396
397    /**
398     * Returns a new {@code CharSequence} which is a subsequence of the
399     * {@code String} representation of this message in the specified locale.
400     * The subsequence starts with the {@code char} value at the specified index
401     * and ends with the {@code char} value at index {@code end - 1} . The
402     * length (in {@code char}s) of the returned sequence is {@code end - start}
403     * , so if {@code start == end} then an empty sequence is returned.
404     *
405     * @param locale
406     *            The locale.
407     * @param start
408     *            The start index, inclusive.
409     * @param end
410     *            The end index, exclusive.
411     * @return The specified subsequence.
412     * @throws IndexOutOfBoundsException
413     *             If {@code start} or {@code end} are negative, if {@code end}
414     *             is greater than {@code length()}, or if {@code start} is
415     *             greater than {@code end}.
416     * @throws NullPointerException
417     *             If {@code locale} was {@code null}.
418     */
419    public CharSequence subSequence(final Locale locale, final int start,
420            final int end) {
421        return toString(locale).subSequence(start, end);
422    }
423
424    /**
425     * Returns the {@code String} representation of this message in the default
426     * locale.
427     *
428     * @return The {@code String} representation of this message.
429     */
430    @Override
431    public String toString() {
432        return toString(Locale.getDefault());
433    }
434
435    /**
436     * Returns the {@code String} representation of this message in the
437     * specified locale.
438     *
439     * @param locale
440     *            The locale.
441     * @return The {@code String} representation of this message.
442     * @throws NullPointerException
443     *             If {@code locale} was {@code null}.
444     */
445    @SuppressWarnings("resource")
446    public String toString(final Locale locale) {
447        String s;
448        final String fmt = descriptor.getFormatString(locale);
449        if (descriptor.requiresFormatter()) {
450            try {
451                // TODO: remove this code once the JDK issue referenced in 3077
452                // is closed.
453                if (IS_AIX_POST5) {
454                    // Java 6 in AIX Formatter does not handle properly
455                    // Formattable arguments; this code is a workaround for the
456                    // problem.
457                    boolean changeType = false;
458                    for (final Object o : args) {
459                        if (o instanceof Formattable) {
460                            changeType = true;
461                            break;
462                        }
463                    }
464                    if (changeType) {
465                        final Object[] newArgs = new Object[args.length];
466                        for (int i = 0; i < args.length; i++) {
467                            if (args[i] instanceof Formattable) {
468                                newArgs[i] = args[i].toString();
469                            } else {
470                                newArgs[i] = args[i];
471                            }
472                        }
473                        s = new Formatter(locale).format(locale, fmt, newArgs)
474                                .toString();
475                    } else {
476                        s = new Formatter(locale).format(locale, fmt, args)
477                                .toString();
478                    }
479                } else {
480                    s = new Formatter(locale).format(locale, fmt, args)
481                            .toString();
482                }
483            } catch (final IllegalFormatException e) {
484                // This should not happen with any of our internal messages.
485                // However, this may happen for raw messages that have a
486                // mismatch between argument specifier type and argument type.
487                s = fmt;
488            }
489        } else {
490            s = fmt;
491        }
492        if (s == null) {
493            s = "";
494        }
495        return s;
496    }
497
498}