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 }