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 }