View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions Copyrighted [year] [name of copyright owner]".
13   *
14   *      Copyright 2009 Sun Microsystems, Inc.
15   *      Portions copyright 2011-2012 ForgeRock AS
16   */
17  
18  package org.forgerock.i18n;
19  
20  import java.io.Serializable;
21  import java.util.Formattable;
22  import java.util.Formatter;
23  import java.util.IllegalFormatException;
24  import java.util.Locale;
25  
26  import org.forgerock.i18n.LocalizableMessageDescriptor.AbstractLocalizableMessageDescriptor;
27  
28  /**
29   * A localizable message whose {@code String} representation can be retrieved in
30   * one or more locales. A message is localized each time it is converted to a
31   * {@code String} using one of its {@link #toString} methods.
32   * <p>
33   * Localizable messages are particularly useful in situations where a message a
34   * destined for multiple recipients, potentially in different locales. For
35   * example, a server application may record a message in its log file using its
36   * default locale, but also send the same message to the client using the
37   * client's locale (if known).
38   * <p>
39   * In most cases messages are intended for use in a locale-sensitive manner
40   * although this class defines convenience methods for creating non-localizable
41   * messages whose {@code String} representation is always the same regardless of
42   * the requested locale.
43   * <p>
44   * This class implements {@code CharSequence} so that messages can be supplied
45   * as arguments to other messages. This way messages can be composed of
46   * fragments of other messages if necessary.
47   *
48   * @see LocalizableMessageBuilder
49   */
50  public final class LocalizableMessage implements CharSequence, Formattable,
51          Comparable<LocalizableMessage>, Serializable {
52  
53      /**
54       * Generated serialization ID.
55       */
56      private static final long serialVersionUID = 8011606572832995899L;
57  
58      /**
59       * Represents an empty message string.
60       */
61      public static final LocalizableMessage EMPTY = LocalizableMessage.raw("");
62  
63      // Variable used to workaround a bug in AIX Java 1.6
64      // TODO: remove this code once the JDK issue referenced in 3077 is
65      // closed.
66      private static final boolean IS_AIX_POST5 = isAIXPost5();
67  
68      /**
69       * Creates an non-localizable message whose {@code String} representation is
70       * always the same regardless of the requested locale.
71       * <p>
72       * Note that the types for {@code args} must be consistent with any argument
73       * specifiers appearing in {@code formatString} according to the rules of
74       * {@link java.util.Formatter}. A mismatch in type information will cause
75       * this message to render without argument substitution. Before using this
76       * method you should be sure that the message you are creating is not locale
77       * sensitive. If it is locale sensitive consider defining an appropriate
78       * {@link LocalizableMessageDescriptor}.
79       * <p>
80       * This method handles the special case where a {@code CharSequence} needs
81       * to be converted directly to {@code LocalizableMessage} as follows:
82       *
83       * <pre>
84       * String s = ...;
85       *
86       * // Both of these are equivalent:
87       * LocalizableMessage m = LocalizableMessage.raw(s);
88       * LocalizableMessage m = LocalizableMessage.raw("%s", s);
89       * </pre>
90       *
91       * @param formatString
92       *            The raw message format string.
93       * @param args
94       *            The raw message parameters.
95       * @return A non-localizable messages whose {@code String} representation is
96       *         always the same regardless of the requested locale.
97       * @throws NullPointerException
98       *             If {@code formatString} was {@code null}.
99       */
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 }